You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

345 lines
10 KiB

internal/libs/protoio: optimize MarshalDelimited by plain byteslice allocations+sync.Pool (#7325) Noticed in profiles that invoking *VoteSignBytes always created a bytes.Buffer, then discarded it inside protoio.MarshalDelimited. I dug further and examined the call paths and noticed that we unconditionally create the bytes.Buffer, even though we might have proto messages (in the common case) that implement MarshalTo([]byte), and invoked varintWriter. Instead by inlining this case, we skip a bunch of allocations and CPU cycles, which then reflects properly on all calling functions. Here are the benchmark results: ```shell $ benchstat before.txt after.txt name old time/op new time/op delta types.VoteSignBytes-8 705ns ± 3% 573ns ± 6% -18.74% (p=0.000 n=18+20) types.CommitVoteSignBytes-8 8.15µs ± 9% 6.81µs ± 4% -16.51% (p=0.000 n=20+19) protoio.MarshalDelimitedWithMarshalTo-8 788ns ± 8% 772ns ± 3% -2.01% (p=0.050 n=20+20) protoio.MarshalDelimitedNoMarshalTo-8 989ns ± 4% 845ns ± 2% -14.51% (p=0.000 n=20+18) name old alloc/op new alloc/op delta types.VoteSignBytes-8 792B ± 0% 600B ± 0% -24.24% (p=0.000 n=20+20) types.CommitVoteSignBytes-8 9.52kB ± 0% 7.60kB ± 0% -20.17% (p=0.000 n=20+20) protoio.MarshalDelimitedNoMarshalTo-8 808B ± 0% 440B ± 0% -45.54% (p=0.000 n=20+20) name old allocs/op new allocs/op delta types.VoteSignBytes-8 13.0 ± 0% 10.0 ± 0% -23.08% (p=0.000 n=20+20) types.CommitVoteSignBytes-8 140 ± 0% 110 ± 0% -21.43% (p=0.000 n=20+20) protoio.MarshalDelimitedNoMarshalTo-8 10.0 ± 0% 7.0 ± 0% -30.00% (p=0.000 n=20+20) ``` Thanks to Tharsis who tasked me to help them increase TPS and who are keen on improving Tendermint and efficiency.
3 years ago
  1. package types
  2. import (
  3. "context"
  4. "testing"
  5. "time"
  6. "github.com/gogo/protobuf/proto"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/require"
  9. "github.com/tendermint/tendermint/crypto"
  10. "github.com/tendermint/tendermint/crypto/ed25519"
  11. "github.com/tendermint/tendermint/crypto/tmhash"
  12. "github.com/tendermint/tendermint/internal/libs/protoio"
  13. tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
  14. )
  15. func examplePrevote() *Vote {
  16. return exampleVote(byte(tmproto.PrevoteType))
  17. }
  18. func examplePrecommit() *Vote {
  19. return exampleVote(byte(tmproto.PrecommitType))
  20. }
  21. func exampleVote(t byte) *Vote {
  22. var stamp, err = time.Parse(TimeFormat, "2017-12-25T03:00:01.234Z")
  23. if err != nil {
  24. panic(err)
  25. }
  26. return &Vote{
  27. Type: tmproto.SignedMsgType(t),
  28. Height: 12345,
  29. Round: 2,
  30. Timestamp: stamp,
  31. BlockID: BlockID{
  32. Hash: tmhash.Sum([]byte("blockID_hash")),
  33. PartSetHeader: PartSetHeader{
  34. Total: 1000000,
  35. Hash: tmhash.Sum([]byte("blockID_part_set_header_hash")),
  36. },
  37. },
  38. ValidatorAddress: crypto.AddressHash([]byte("validator_address")),
  39. ValidatorIndex: 56789,
  40. }
  41. }
  42. func TestVoteSignable(t *testing.T) {
  43. vote := examplePrecommit()
  44. v := vote.ToProto()
  45. signBytes := VoteSignBytes("test_chain_id", v)
  46. pb := CanonicalizeVote("test_chain_id", v)
  47. expected, err := protoio.MarshalDelimited(&pb)
  48. require.NoError(t, err)
  49. require.Equal(t, expected, signBytes, "Got unexpected sign bytes for Vote.")
  50. }
  51. func TestVoteSignBytesTestVectors(t *testing.T) {
  52. tests := []struct {
  53. chainID string
  54. vote *Vote
  55. want []byte
  56. }{
  57. 0: {
  58. "", &Vote{},
  59. // NOTE: Height and Round are skipped here. This case needs to be considered while parsing.
  60. []byte{0xd, 0x2a, 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1},
  61. },
  62. // with proper (fixed size) height and round (PreCommit):
  63. 1: {
  64. "", &Vote{Height: 1, Round: 1, Type: tmproto.PrecommitType},
  65. []byte{
  66. 0x21, // length
  67. 0x8, // (field_number << 3) | wire_type
  68. 0x2, // PrecommitType
  69. 0x11, // (field_number << 3) | wire_type
  70. 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height
  71. 0x19, // (field_number << 3) | wire_type
  72. 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round
  73. 0x2a, // (field_number << 3) | wire_type
  74. // remaining fields (timestamp):
  75. 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1},
  76. },
  77. // with proper (fixed size) height and round (PreVote):
  78. 2: {
  79. "", &Vote{Height: 1, Round: 1, Type: tmproto.PrevoteType},
  80. []byte{
  81. 0x21, // length
  82. 0x8, // (field_number << 3) | wire_type
  83. 0x1, // PrevoteType
  84. 0x11, // (field_number << 3) | wire_type
  85. 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height
  86. 0x19, // (field_number << 3) | wire_type
  87. 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round
  88. 0x2a, // (field_number << 3) | wire_type
  89. // remaining fields (timestamp):
  90. 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1},
  91. },
  92. 3: {
  93. "", &Vote{Height: 1, Round: 1},
  94. []byte{
  95. 0x1f, // length
  96. 0x11, // (field_number << 3) | wire_type
  97. 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height
  98. 0x19, // (field_number << 3) | wire_type
  99. 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round
  100. // remaining fields (timestamp):
  101. 0x2a,
  102. 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1},
  103. },
  104. // containing non-empty chain_id:
  105. 4: {
  106. "test_chain_id", &Vote{Height: 1, Round: 1},
  107. []byte{
  108. 0x2e, // length
  109. 0x11, // (field_number << 3) | wire_type
  110. 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height
  111. 0x19, // (field_number << 3) | wire_type
  112. 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round
  113. // remaining fields:
  114. 0x2a, // (field_number << 3) | wire_type
  115. 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1, // timestamp
  116. // (field_number << 3) | wire_type
  117. 0x32,
  118. 0xd, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64}, // chainID
  119. },
  120. }
  121. for i, tc := range tests {
  122. v := tc.vote.ToProto()
  123. got := VoteSignBytes(tc.chainID, v)
  124. assert.Equal(t, len(tc.want), len(got), "test case #%v: got unexpected sign bytes length for Vote.", i)
  125. assert.Equal(t, tc.want, got, "test case #%v: got unexpected sign bytes for Vote.", i)
  126. }
  127. }
  128. func TestVoteProposalNotEq(t *testing.T) {
  129. cv := CanonicalizeVote("", &tmproto.Vote{Height: 1, Round: 1})
  130. p := CanonicalizeProposal("", &tmproto.Proposal{Height: 1, Round: 1})
  131. vb, err := proto.Marshal(&cv)
  132. require.NoError(t, err)
  133. pb, err := proto.Marshal(&p)
  134. require.NoError(t, err)
  135. require.NotEqual(t, vb, pb)
  136. }
  137. func TestVoteVerifySignature(t *testing.T) {
  138. privVal := NewMockPV()
  139. pubkey, err := privVal.GetPubKey(context.Background())
  140. require.NoError(t, err)
  141. vote := examplePrecommit()
  142. v := vote.ToProto()
  143. signBytes := VoteSignBytes("test_chain_id", v)
  144. // sign it
  145. err = privVal.SignVote(context.Background(), "test_chain_id", v)
  146. require.NoError(t, err)
  147. // verify the same vote
  148. valid := pubkey.VerifySignature(VoteSignBytes("test_chain_id", v), v.Signature)
  149. require.True(t, valid)
  150. // serialize, deserialize and verify again....
  151. precommit := new(tmproto.Vote)
  152. bs, err := proto.Marshal(v)
  153. require.NoError(t, err)
  154. err = proto.Unmarshal(bs, precommit)
  155. require.NoError(t, err)
  156. // verify the transmitted vote
  157. newSignBytes := VoteSignBytes("test_chain_id", precommit)
  158. require.Equal(t, string(signBytes), string(newSignBytes))
  159. valid = pubkey.VerifySignature(newSignBytes, precommit.Signature)
  160. require.True(t, valid)
  161. }
  162. func TestIsVoteTypeValid(t *testing.T) {
  163. tc := []struct {
  164. name string
  165. in tmproto.SignedMsgType
  166. out bool
  167. }{
  168. {"Prevote", tmproto.PrevoteType, true},
  169. {"Precommit", tmproto.PrecommitType, true},
  170. {"InvalidType", tmproto.SignedMsgType(0x3), false},
  171. }
  172. for _, tt := range tc {
  173. tt := tt
  174. t.Run(tt.name, func(st *testing.T) {
  175. if rs := IsVoteTypeValid(tt.in); rs != tt.out {
  176. t.Errorf("got unexpected Vote type. Expected:\n%v\nGot:\n%v", rs, tt.out)
  177. }
  178. })
  179. }
  180. }
  181. func TestVoteVerify(t *testing.T) {
  182. privVal := NewMockPV()
  183. pubkey, err := privVal.GetPubKey(context.Background())
  184. require.NoError(t, err)
  185. vote := examplePrevote()
  186. vote.ValidatorAddress = pubkey.Address()
  187. err = vote.Verify("test_chain_id", ed25519.GenPrivKey().PubKey())
  188. if assert.Error(t, err) {
  189. assert.Equal(t, ErrVoteInvalidValidatorAddress, err)
  190. }
  191. err = vote.Verify("test_chain_id", pubkey)
  192. if assert.Error(t, err) {
  193. assert.Equal(t, ErrVoteInvalidSignature, err)
  194. }
  195. }
  196. func TestVoteString(t *testing.T) {
  197. str := examplePrecommit().String()
  198. expected := `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PRECOMMIT(Precommit) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}`
  199. if str != expected {
  200. t.Errorf("got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str)
  201. }
  202. str2 := examplePrevote().String()
  203. expected = `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PREVOTE(Prevote) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}`
  204. if str2 != expected {
  205. t.Errorf("got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str2)
  206. }
  207. }
  208. func TestVoteValidateBasic(t *testing.T) {
  209. privVal := NewMockPV()
  210. testCases := []struct {
  211. testName string
  212. malleateVote func(*Vote)
  213. expectErr bool
  214. }{
  215. {"Good Vote", func(v *Vote) {}, false},
  216. {"Negative Height", func(v *Vote) { v.Height = -1 }, true},
  217. {"Negative Round", func(v *Vote) { v.Round = -1 }, true},
  218. {"Invalid BlockID", func(v *Vote) {
  219. v.BlockID = BlockID{[]byte{1, 2, 3}, PartSetHeader{111, []byte("blockparts")}}
  220. }, true},
  221. {"Invalid Address", func(v *Vote) { v.ValidatorAddress = make([]byte, 1) }, true},
  222. {"Invalid ValidatorIndex", func(v *Vote) { v.ValidatorIndex = -1 }, true},
  223. {"Invalid Signature", func(v *Vote) { v.Signature = nil }, true},
  224. {"Too big Signature", func(v *Vote) { v.Signature = make([]byte, MaxSignatureSize+1) }, true},
  225. }
  226. for _, tc := range testCases {
  227. tc := tc
  228. t.Run(tc.testName, func(t *testing.T) {
  229. vote := examplePrecommit()
  230. v := vote.ToProto()
  231. err := privVal.SignVote(context.Background(), "test_chain_id", v)
  232. vote.Signature = v.Signature
  233. require.NoError(t, err)
  234. tc.malleateVote(vote)
  235. assert.Equal(t, tc.expectErr, vote.ValidateBasic() != nil, "Validate Basic had an unexpected result")
  236. })
  237. }
  238. }
  239. func TestVoteProtobuf(t *testing.T) {
  240. privVal := NewMockPV()
  241. vote := examplePrecommit()
  242. v := vote.ToProto()
  243. err := privVal.SignVote(context.Background(), "test_chain_id", v)
  244. vote.Signature = v.Signature
  245. require.NoError(t, err)
  246. testCases := []struct {
  247. msg string
  248. v1 *Vote
  249. expPass bool
  250. }{
  251. {"success", vote, true},
  252. {"fail vote validate basic", &Vote{}, false},
  253. {"failure nil", nil, false},
  254. }
  255. for _, tc := range testCases {
  256. protoProposal := tc.v1.ToProto()
  257. v, err := VoteFromProto(protoProposal)
  258. if tc.expPass {
  259. require.NoError(t, err)
  260. require.Equal(t, tc.v1, v, tc.msg)
  261. } else {
  262. require.Error(t, err)
  263. }
  264. }
  265. }
  266. var sink interface{}
  267. var protoVote *tmproto.Vote
  268. var sampleCommit *Commit
  269. func init() {
  270. protoVote = examplePrecommit().ToProto()
  271. lastID := makeBlockIDRandom()
  272. voteSet, _, vals := randVoteSet(2, 1, tmproto.PrecommitType, 10, 1)
  273. commit, err := makeCommit(lastID, 2, 1, voteSet, vals, time.Now())
  274. if err != nil {
  275. panic(err)
  276. }
  277. sampleCommit = commit
  278. }
  279. func BenchmarkVoteSignBytes(b *testing.B) {
  280. b.ReportAllocs()
  281. for i := 0; i < b.N; i++ {
  282. sink = VoteSignBytes("test_chain_id", protoVote)
  283. }
  284. if sink == nil {
  285. b.Fatal("Benchmark did not run")
  286. }
  287. // Reset the sink.
  288. sink = (interface{})(nil)
  289. }
  290. func BenchmarkCommitVoteSignBytes(b *testing.B) {
  291. b.ReportAllocs()
  292. for i := 0; i < b.N; i++ {
  293. for index := range sampleCommit.Signatures {
  294. sink = sampleCommit.VoteSignBytes("test_chain_id", int32(index))
  295. }
  296. }
  297. if sink == nil {
  298. b.Fatal("Benchmark did not run")
  299. }
  300. // Reset the sink.
  301. sink = (interface{})(nil)
  302. }