Add delta-compression using an operation log#700
Conversation
|
Sounds awesome, I like the idea! I'll review tomorrow (it's 2:30 AM for me 😅). |
|
Wait, can't we already do this with ordered events? They're buffered by the backend and can represent operations. For example, what if, instead of using a special One downside - right now this won't take visibility into account. If you use What do you think about this? Am I missing something? |
|
I think that in general it's preferred to keep all replication-related code in replication-land instead of message-land, since over time replication might accumulate more changes that only work with replication-messages. Some issues with implementing this via messages:
|
|
Agreed. Let's proceed with your approach. I'll finish my review in a few minutes. And I'll open an issue about visibility, I think it's something that we should handle regardless.
|
There was a problem hiding this comment.
Hi! I think your solution is much stronger than what I was thinking. I tried going for a snapshot solution (think video encoding), by trading possible correctness for a smaller memory footprint (and leaving out diffs for variable length types). For my use case, I would have argued it had its worth; but I am also interested to see how it plays out with this solution.
Perhaps in the future I hope to argue for the snapshot strategy again.
I hope to test it out soon!
deb3e48 to
a0f21ed
Compare
|
@Shatur should be ready to review! |
Shatur
left a comment
There was a problem hiding this comment.
Thanks! Left a few small suggestions / questions.
Lighyear implemented delta compression by providing an API to compute diffs between two component states, keeping track of past component states and manually sending diffs from the last ACK-ed state for each client. This was inefficient for the common case of sending a `Vec<Point>` or `VecDeque<Point>`, i.e. an appendable data structure, because we had to compute diffs in an expensive manner and we had to store the full vec for a lot of past ticks. This PR implements delta-compression with a different approach: - the user has to provide methods to manually modify the component that they want to replicate, and use that in their code. This lets us avoid the expensive diff-computation form state since the user directly provides the diffs! - replicon can then send those diffs, along with an u64 cursor to make sure that a received diff is not applied multiple times Change-Id: I3869167e660a403f37480c1d1daef1f088b06ca5
Change-Id: I686ce4d6eb09a563351862d4306775b46de45b6e
Change-Id: I64ed3af039e3c72b5545ae194f806597eb8372cc
Change-Id: Ic0c22b920c24660359149813fbcd311de9378c0e
…rite. This means we need to add a cursor index on the component itself. Change-Id: Ia2561bb0111e53339ad65d945f796c9bb26eb79a
Change-Id: I765f281420128dc894ae874d5cb97d980f96dd9e
Change-Id: I32a34846de52ee815628845e3e40a0260c7e575b
The diff-log was assigning a new patch index for each individual diff. That means a component that could add 1000 new points in one tick would increment the cursor by 1000! The DIFF_HISTORY_LEN is then hard to choose and very component-specific. Instead we now only increment the patch index when we send the component. We could probably lower the history value Change-Id: I7009af651f16407fe159aaacb0d1bedabfd06bfb
Change-Id: I7c7dc584d45abc0b1e0a0690a4472c4075dce2c8
Change-Id: Iff38b0c401a4f64fd570c6c56a4f7b679e5033ec
Change-Id: Ifda8d3a8b1c843c3ee5300738abf32af88119fa7
f234949 to
9429674
Compare
Change-Id: I49dc4af018ffb4299bf61050e7cd32adff2e1d3f
Change-Id: I3d6b242b9e4bcdfea18da46bc174fd7a82fe6643
Shatur
left a comment
There was a problem hiding this comment.
LGTM! Tomorrow I'll do the mentioned changes and merge it 🙂
It logically belongs here + allows to avoid one extra lookup during param initialization. One `expect` is unavodiable with any approach anyway.
While it doesn't make much sense, it's probably better to keep it for now to avoid itnroducing a breacking change.
Less complicated.
Shorter, just a preference.
No reason to fill the `first_index` if it will be discarded. I think it simpler this way.
Used only in one place where we no longer need to unwrap.
Replace substract with cursor increment. Since we no longer need to substract, > 0 check is no longer needed. Also I this reads better as for my taste.
|
Got a bit busy with life stuff today, so I didn't finish, but pushed a few small changes. I'll wrap this up tomorrow 🙂 |
Previously, we used `u64`, which is unlikely to ever overflow. Even at 60 Hz, it would take ~9.7 billion years. But the code used checked operations, even though they would not help. I was going to switch to regular operations, but realized that if we handle overflows properly, we could save some traffic. By default, integers in postcard are serialized with varint encoding. > 128 takes 2 bytes, > 16384 takes 3 bytes, and so on. But we can switch to `u16` with overflow handling and fixint encoding, which always takes 2 bytes. This also simplifies the logic inside `batches_after`. I also had to replace `BTreeMap` with `HashMap`, but we don't need to preserve the ordering since we iterate over indices, so it's fine.
4a20cad to
d751b3a
Compare
It's easier to understand and faster.
I still want to rework the mutation serialization, but it's a good step in this direction.
b16e3d7 to
6e7d6d4
Compare
|
Done! Notable changes that might affect you:
I’d probably recommend taking another look at the PR diff. It's not big, most of the Please double-check that this approach works for Lightyear. |
We're planning to implement client-server replication.
Lighyear implemented delta compression by providing an API to compute diffs between two component states, keeping track of past component states and manually sending diffs from the last ACK-ed state for each client.
This was inefficient for the common case of sending a
Vec<Point>orVecDeque<Point>, i.e. an appendable data structure, because we had to compute diffs in an expensive manner (diffs between 2 vecs) and we had to store the full vec state for a lot of past ticks.This PR implements delta-compression with a different approach:
On the send-side: we only send the diffs since the last acked-diff.
On the receive-size: we store received diffs in a buffer, and only apply them in order.
I've tried it in a personal game where I need to replicate Vec and I got ~80% reduction in bandwidth