Skip to content

Conversation

@algorandskiy
Copy link
Contributor

@algorandskiy algorandskiy commented Sep 26, 2022

Summary

Inproduce proposal payload compression in protocol version 2.2.

Acceptance:

  1. Unit tests incl 2.1 and 2.2 peers mix
  2. All e2e tests pass
  3. Manual test master and feature nodes on TwoNodes50Each template

Testing

  • Unit test checking old + new ws protocol combinations
  • Manual TwoNotes50Each test with master (m) + feature (f)
  • Manual ThreeNodesEvenDist test with [m]m+f, m+f[f] where [r] is a relay node
  • Perf test

Possible optimization

If all peers support compressed proposals, do not allocate memory for non-compressed data batch. Possibly in another PR.

@jannotti
Copy link
Contributor

zstd seems like a very good choice, since we can seed the dictionary it uses. it ought to do a great job on our repetitive msgpack txns after that.

@codecov
Copy link

codecov bot commented Sep 26, 2022

Codecov Report

Merging #4589 (3f7d72f) into master (bb7c59f) will increase coverage by 0.07%.
The diff coverage is 82.26%.

@@            Coverage Diff             @@
##           master    #4589      +/-   ##
==========================================
+ Coverage   54.34%   54.42%   +0.07%     
==========================================
  Files         402      403       +1     
  Lines       51793    51917     +124     
==========================================
+ Hits        28149    28255     +106     
- Misses      21276    21284       +8     
- Partials     2368     2378      +10     
Impacted Files Coverage Δ
network/wsPeer.go 68.93% <76.31%> (+3.42%) ⬆️
network/msgCompressor.go 82.35% <82.35%> (ø)
network/wsNetwork.go 65.52% <86.53%> (+0.95%) ⬆️
crypto/merkletrie/trie.go 66.42% <0.00%> (-2.19%) ⬇️
crypto/merkletrie/node.go 91.62% <0.00%> (-1.87%) ⬇️
data/transactions/verify/txn.go 76.19% <0.00%> (-0.96%) ⬇️
ledger/tracker.go 77.87% <0.00%> (-0.86%) ⬇️
catchup/service.go 68.14% <0.00%> (-0.75%) ⬇️
ledger/acctupdates.go 70.19% <0.00%> (+0.59%) ⬆️
... and 2 more

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@algorandskiy algorandskiy changed the title WIP: proposal payload compression Proposal payload compression Oct 11, 2022
@algorandskiy algorandskiy changed the title Proposal payload compression network: proposal payload compression Oct 11, 2022
@algorandskiy
Copy link
Contributor Author

Some refactoring, unit tests and rebase to master

jannotti
jannotti previously approved these changes Oct 13, 2022
Copy link
Contributor

@jannotti jannotti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so familiar with wsNetwork, but the parts I understood seemed good.

I wonder, since we have to support uncompressed senders anyway, why should we ever accept a compression that is bigger than the message? We might as well use the uncompressed form in that case. This simplifies the compression routine slightly, because we don't need to ask for the bound, we can just insist that it compresses in less than len(d) or refuse.

func (dec zstdProposalDecompressor) convert(data []byte) ([]byte, error) {
r := zstd.NewReader(bytes.NewReader(data))
defer r.Close()
b := make([]byte, 0, 1024)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though we are only compressing large messages, so maybe start higher? We could define a constant minCompressedMsgSize?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we compressing all proposals, small and large. I think I like this idea, the magic check above would handle both compressed and non-compressed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, right, mspacked, my bad

Copy link
Contributor

@cce cce Oct 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could make the compressed encoding start with an additional 4 bytes to include the length up front?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was considering suggesting that too. Then you could io.ReadFull on the known size (allocated all at once). And the check for exploding messages isn't even needed (it'll just fail to decompress into the buffer, which presumably you've prechecked against the maxsize)

Copy link
Contributor

@jannotti jannotti Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, how about we start with a buffer 5x the size of the compressed message?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe 2.5x would be a good guess since we see 2-3x compression ratio

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set to 3x

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. That should eliminate most copies.

Copy link
Contributor Author

@algorandskiy algorandskiy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This simplifies the compression routine slightly, because we don't need to ask for the bound, we can just insist that it compresses in less than len(d) or refuse.

I thought about it, and the only way is to compare the post-compression result (since the bound is always greater, and internally zstd re-allocates).

func (dec zstdProposalDecompressor) convert(data []byte) ([]byte, error) {
r := zstd.NewReader(bytes.NewReader(data))
defer r.Close()
b := make([]byte, 0, 1024)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we compressing all proposals, small and large. I think I like this idea, the magic check above would handle both compressed and non-compressed

@algorandskiy algorandskiy requested review from cce and jannotti October 13, 2022 20:12
@zeldovich
Copy link
Contributor

A common pattern for algod is to take an incoming message and relay it to others. Should we save the compressed message in network.IncomingMessage so that we don't have to re-compress it if we decide to re-broadcast it to other peers?

@algorandskiy
Copy link
Contributor Author

A common pattern for algod is to take an incoming message and relay it to others. Should we save the compressed message in network.IncomingMessage so that we don't have to re-compress it if we decide to re-broadcast it to other peers?

outmsg := wn.handlers.Handle(msg)
...
case Broadcast:
	err := wn.Broadcast(wn.ctx, msg.Tag, msg.Data, false, msg.Sender)

Incoming msg gets converted into Outgoing msg, so re-broadcasting is a case only for some messages. Looks like we might indeed store the compressed part in Incoming and let the handler to propagate it into Outgoing. The only issue we start working with bytes at broadsting level, not with messages. I'll look close into it.

@cce
Copy link
Contributor

cce commented Oct 13, 2022

@zeldovich the issue with proposals is that the relayAction constructs a new transmittedPayload by combining a PriorVote and unauthenticatedProposal that were pulled separately from the voteTracker and proposalStore systems — I ran into this when doing a POC to avoid the re-msgp-encoding of proposals when relaying a proposal in #4568.

I ran into the issue where I believe it isn't totally guaranteed that the PriorVote that was sent to you in the PP/transmittedPayload message (that you fed into agreement by splitting it in two messageEvents) would be the same PriorVote as you send out when relaying the PP/transmittedPayload message. Or maybe it is always the same when you are relaying for period 0?

In any case you can see in my POC I ended up adding references to the original msgp encoding of the unauthenticatedProposal and PriorVote separately, and then do a byte-comparison on PriorVote just to be extra sure it is the same before re-using the encoding...
#4568

But perhaps it would be possible to similarly feed the compressed proposal message into agreement and back out again to avoid re-compressing.

@zeldovich
Copy link
Contributor

Hmm, yeah, on closer examination it seems like it will be tricky to do this optimization. You might be right that, in period 0, the prior vote that accompanied the proposal on arrival is the same as (or just as good as?) the prior vote we pull out of the voteTracker, but this also seems a subtle invariant to think about..

I was thinking it would be doable because we try to track some information about the original incoming message (e.g., agreement.MessageHandle), but it seems not sufficient for what we're talking about.

Not worth delaying this PR for this second-order optimization.

@algorandskiy
Copy link
Contributor Author

According to perf tests, PP traffic went down to about 30%, and round time shows less fluctuations.

@cce
Copy link
Contributor

cce commented Oct 14, 2022

@zeldovich I guess one thing we could do, but would require a consensus upgrade, is move the compression into the networkAction.do code where we are calling protocol.Encode before sending out a proposal. We could have a new transmittedPayload encoding that was something like

type transmittedPayload struct {
   CompressedEncodedProposal []byte `codec:"pp"`
   PriorVote unauthenticatedVote `codec:"pv"`
}

and it would be agreement's job to decompress the proposal, and also keep a reference to the original compressed and msgp-encoded version in the unauthenticatedProposal type like I had in #4568.

Then we would only be compressing the proposal and not the PriorVote, so we wouldn't have to worry about mixing them up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants