Skip to content
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

xdr: update using xdrgen#65 #3957

Merged
merged 18 commits into from
Nov 8, 2021
Merged

xdr: update using xdrgen#65 #3957

merged 18 commits into from
Nov 8, 2021

Conversation

leighmcculloch
Copy link
Member

PR Checklist

PR Structure

  • This PR has reasonably narrow scope (if not, break it down into smaller PRs).
  • This PR avoids mixing refactoring changes with feature changes (split into two PRs
    otherwise).
  • This PR's title starts with name of package that is most changed in the PR, ex.
    services/friendbot, or all or doc if the changes are broad or impact many
    packages.

Thoroughness

  • This PR adds tests for the most critical parts of the new functionality or fixes.
  • I've updated any docs (developer docs, .md
    files, etc... affected by this change). Take a look in the docs folder for a given service,
    like this one.

Release planning

  • I've updated the relevant CHANGELOG (here for Horizon) if
    needed with deprecations, added features, breaking changes, and DB schema changes.
  • I've decided if this PR requires a new major/minor version according to
    semver, or if it's mainly a patch change. The PR is targeted at the next
    release branch if it's not a patch change.

What

Update XDR using stellar/xdrgen#65 that improves the encoding cpu and memory usage.

Why

See discussion on stellar/xdrgen#65.

@leighmcculloch leighmcculloch marked this pull request as ready for review October 6, 2021 00:03
@leighmcculloch leighmcculloch requested a review from bartekn October 6, 2021 00:03
@leighmcculloch
Copy link
Member Author

@bartekn I've fixed the linter errors in this PR.

@bartekn
Copy link
Contributor

bartekn commented Oct 6, 2021

Started verify-range on last 100k ledgers: [37581877, 37681877]. I'll have results in a few hours.

@bartekn
Copy link
Contributor

bartekn commented Oct 6, 2021

@leighmcculloch good news, verify-range found no changes so the new code looks good (at least for tx related structs). I think you can also consider fuzzing it a little bit, @tamirms created this great tool some time ago: https://github.com/stellar/go/tree/master/randxdr. It allows you to generate random XDR objects. Here's an example how to use it.

@leighmcculloch leighmcculloch self-assigned this Oct 19, 2021
@2opremio
Copy link
Contributor

2opremio commented Oct 20, 2021

@leighmcculloch would you mind rebasing? I did so at https://github.com/stellar/go/tree/perf-xdr myself but I cannot force-push to your branch.

@leighmcculloch
Copy link
Member Author

@2opremio I've merged master into this PR, and rerun stellar/xdrgen#65 on the .x files that live in this repo.

@bartekn
Copy link
Contributor

bartekn commented Nov 5, 2021

@stellar/horizon-committers do we want to include this in next Horizon release. This time we have full week of testing.

@ire-and-curses
Copy link
Member

I think so - if we can also do fuzzing in parallel to our usual testing this week. @leighmcculloch would you be able to do that?

@leighmcculloch
Copy link
Member Author

@ire-and-curses I can't dedicate anymore time to this in the immediate future, nor do I have the expertise the Horizon team does in fuzzing this in the context of Horizon's use cases. If your team can do that, that would be very helpful.

@bartekn
Copy link
Contributor

bartekn commented Nov 5, 2021

OK, we're going to take care of this. @stellar/horizon-committers whoever is testing next week please merge this into release branch.

@leighmcculloch
Copy link
Member Author

I can get this branch updated to target the release branch. Just let me know which branch and when ready. I'll merge the xdrgen change that is still open.

@bartekn
Copy link
Contributor

bartekn commented Nov 5, 2021

The release branch will be branched off master so if you fix conflicts with master it should be enough. Thanks!

Copy link
Member Author

@leighmcculloch leighmcculloch left a comment

Choose a reason for hiding this comment

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

@bartekn The PR is updated with master now. Let me know if anything else needs changing.

There's also one thing I noticed that might be another opportunity for improvement I'll try at a later date.

Comment on lines 17634 to 17638
func (s TransactionV1Envelope) MarshalBinary() ([]byte, error) {
b := new(bytes.Buffer)
_, err := Marshal(b, s)
b := bytes.Buffer{}
e := xdr.NewEncoder(&b)
err := s.EncodeTo(e)
return b.Bytes(), err
Copy link
Member Author

Choose a reason for hiding this comment

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

@2opremio I've been thinking about the work you did recently in stellar/go-xdr#16 and wondering if we can apply the same pattern you came up with to use a predefined fixed size buffer here too.

One of the downsides of using bytes.Buffer is that it will start with a smallish sized buffer and keep growing the buffer, which is probably allocations on the heap.

Many of our XDRs structs have a well defined maximum size because most of our variable length fields have a small maximum set. We could replace bytes.Buffer with our own fixed size buffers. This might not work if the buffers are to big to fit on the stack.

For example, if the max size of a transaction is 1kb, we could define a type:

type FixedBuffer1024 struct {
	buf [1024]byte
	len int
}

func (b *FixedBuffer1024) Write(b []byte) (int, error) {
	// ...
}

For each sized buffer we need we define a new type. With Go generics one day we could probably reduce all these down to one type, but we don't need generics to do it since we're already code generating.

Do you think this would help? I can try it out when I come back to xdrgen to do the decoding.

cc @bartekn

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need new structs, though? What about:

b := make([]byte, 0, 1024)

We can then pass to a single struct working on a provided slice? I think it will work the same wrt to allocations and we won't need to have so many types.

Copy link
Contributor

@2opremio 2opremio Nov 6, 2021

Choose a reason for hiding this comment

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

I think this would benefit even more from a variant of MarshalBinary() which writes the output to a buffer you pass:

func (s TransactionV1Envelope) MarshalBinaryToBuffer(b *bytes.Buffer) error {
	e := xdr.NewEncoder(&b)
	err := s.EncodeTo(e)
	return b.Bytes(), err
}

// less performant, but convenient
func (s TransactionV1Envelope) MarshalBinary() ([]byte, error) { 
	b := bytes.NewBuffer(make([]byte, somenumber)) // as indicated by bartek, somenumber should (generously) approximate the estimated size of the output. We would rather consume a bit more memory, but reduce the allocations
	err := s.MarshalBinaryToBuffer(&b)
	return b.Bytes(), err
}

In that way, a consumer which decodes continuously (which, I think we have a few) would be able to reuse the buffer across calls, considerably reducing allocations:

func ProcessTransactions() {
	var b bytes.Buffer  // TODO: It would be even better to use a buffer with a reasonable
				       //             initial size, maybe we could generate EstimatedBinarySize()
				       //             methods for the xdr types? (it's probably an overoptimization though)
	for {
		t := getTransactionEnvelope()
		b.Reset() // make sure the existing underlying buffer is reused
		t.MarshalBinaryToBuffer(&b)
		... // do something with b.Bytes()
	} 
}

We could even add a reset method to the Encoder/Decoder to replace the underlying reader and writer to reduce the allocations even further.

func ProcessTransactions() {
	var b bytes.Buffer  // TODO: It would be even better to use a buffer with a reasonable
				       //             initial size, maybe we could generate EstimatedBinarySize()
				       //             methods for the xdr types? (it's probably an overoptimization though)
        var e xdr.Encoder
	for {
		t := getTransactionEnvelope()
		b.Reset() // make sure the existing underlying buffer is reused
		e.Reset(&b) // maybe not even necessary since the underlying reader didn't change
		s.EncodeTo(e)
		... // do something with b.Bytes()
		
	} 
}

I think I will try doing something like this for functions in the critical path and see whether it makes a big difference. Then we can look into how to productize if necessary (it's probably fine to simply not use MarshalBinary for things in the critical path)

@leighmcculloch thanks for fostering brainstorming about this!

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes, those are better ideas. If we can share a buffer let's do it. I think your comment is correct, we don't need the reset function on the encoder since in your example there's already the reset being used on the buffer. The encoder stores no meaningful state. So I think we have everything we need already to do the reuse. 🎉

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we track this idea somewhere? New issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

I have something better. A PR implementing it :)

Copy link
Contributor

Choose a reason for hiding this comment

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

@2opremio
Copy link
Contributor

2opremio commented Nov 8, 2021

@leighmcculloch I rerun a spurious failing test in Circle and now everything passes. Could you rebase again (sorry :S) and merge right after it succeeds?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants