diff --git a/lite/helpers.go b/lite/helpers.go index 9c015a08e..fc4d697ae 100644 --- a/lite/helpers.go +++ b/lite/helpers.go @@ -78,6 +78,9 @@ func (v ValKeys) signHeader(header *types.Header, first, last int) *types.Commit // fill in the votes we want for i := first; i < last; i++ { + if i >= len(v) { + break + } vote := makeVote(header, vset, v[i]) votes[vote.ValidatorIndex] = vote } diff --git a/lite/memprovider.go b/lite/memprovider.go index ed7cd7725..bfd260ce5 100644 --- a/lite/memprovider.go +++ b/lite/memprovider.go @@ -14,6 +14,8 @@ type memStoreProvider struct { // btree would be more efficient for larger sets byHeight fullCommits byHash map[string]FullCommit + + sorted bool } // fullCommits just exists to allow easy sorting @@ -52,7 +54,7 @@ func (m *memStoreProvider) StoreCommit(fc FullCommit) error { defer m.mtx.Unlock() m.byHash[key] = fc m.byHeight = append(m.byHeight, fc) - sort.Sort(m.byHeight) + m.sorted = false return nil } @@ -60,17 +62,56 @@ func (m *memStoreProvider) StoreCommit(fc FullCommit) error { func (m *memStoreProvider) GetByHeight(h int64) (FullCommit, error) { m.mtx.RLock() defer m.mtx.RUnlock() - + if !m.sorted { + sort.Sort(m.byHeight) + m.sorted = true + } // search from highest to lowest for i := len(m.byHeight) - 1; i >= 0; i-- { - fc := m.byHeight[i] - if fc.Height() <= h { + if fc := m.byHeight[i]; fc.Height() <= h { return fc, nil } } return FullCommit{}, liteErr.ErrCommitNotFound() } +// GetByHeight returns the FullCommit for height h or an error if the commit is not found. +func (m *memStoreProvider) GetByHeightBinarySearch(h int64) (FullCommit, error) { + m.mtx.RLock() + defer m.mtx.RUnlock() + if !m.sorted { + sort.Sort(m.byHeight) + m.sorted = true + } + low, high := 0, len(m.byHeight)-1 + var mid int + var hmid int64 + var midFC FullCommit + // Our goal is to either find: + // * item ByHeight with the query + // * heighest height with a height <= query + for low <= high { + mid = int(uint(low+high) >> 1) // Avoid an overflow + midFC = m.byHeight[mid] + hmid = midFC.Height() + switch { + case hmid == h: + return midFC, nil + case hmid < h: + low = mid + 1 + case hmid > h: + high = mid - 1 + } + } + + if high >= 0 { + if highFC := m.byHeight[high]; highFC.Height() < h { + return highFC, nil + } + } + return FullCommit{}, liteErr.ErrCommitNotFound() +} + // GetByHash returns the FullCommit for the hash or an error if the commit is not found. func (m *memStoreProvider) GetByHash(hash []byte) (FullCommit, error) { m.mtx.RLock() diff --git a/lite/performance_test.go b/lite/performance_test.go index 28c73bb08..e91671292 100644 --- a/lite/performance_test.go +++ b/lite/performance_test.go @@ -2,6 +2,7 @@ package lite_test import ( "fmt" + "math/rand" "testing" "github.com/tendermint/tendermint/lite" @@ -115,3 +116,112 @@ func benchmarkCertifyCommit(b *testing.B, keys lite.ValKeys) { } } + +type algo bool + +const ( + linearSearch = true + binarySearch = false +) + +var ( + fcs5, h5 = genFullCommits(nil, nil, 5) + fcs50, h50 = genFullCommits(fcs5, h5, 50) + fcs100, h100 = genFullCommits(fcs50, h50, 100) + fcs500, h500 = genFullCommits(fcs100, h100, 500) + fcs1000, h1000 = genFullCommits(fcs500, h500, 1000) +) + +func BenchmarkMemStoreProviderGetByHeightLinearSearch5(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs5, h5, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch50(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs50, h50, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch100(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs100, h100, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch500(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs500, h500, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch1000(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs1000, h1000, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch5(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs5, h5, binarySearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch50(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs50, h50, binarySearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch100(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs100, h100, binarySearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch500(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs500, h500, binarySearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch1000(b *testing.B) { + benchmarkMemStoreProviderGetByHeight(b, fcs1000, h1000, binarySearch) +} + +var rng = rand.New(rand.NewSource(10)) + +func benchmarkMemStoreProviderGetByHeight(b *testing.B, fcs []lite.FullCommit, fHeights []int64, algo algo) { + b.StopTimer() + mp := lite.NewMemStoreProvider() + for i, fc := range fcs { + if err := mp.StoreCommit(fc); err != nil { + b.Fatalf("FullCommit #%d: err: %v", i, err) + } + } + qHeights := make([]int64, len(fHeights)) + copy(qHeights, fHeights) + // Append some non-existent heights to trigger the worst cases. + qHeights = append(qHeights, 19, -100, -10000, 1e7, -17, 31, -1e9) + + searchFn := mp.GetByHeight + if algo == binarySearch { + searchFn = mp.(interface { + GetByHeightBinarySearch(h int64) (lite.FullCommit, error) + }).GetByHeightBinarySearch + } + + hPerm := rng.Perm(len(qHeights)) + b.StartTimer() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, j := range hPerm { + h := qHeights[j] + if _, err := searchFn(h); err != nil { + } + } + } + b.ReportAllocs() +} + +func genFullCommits(prevFC []lite.FullCommit, prevH []int64, want int) ([]lite.FullCommit, []int64) { + fcs := make([]lite.FullCommit, len(prevFC)) + copy(fcs, prevFC) + heights := make([]int64, len(prevH)) + copy(heights, prevH) + + appHash := []byte("benchmarks") + chainID := "benchmarks-gen-full-commits" + n := want + keys := lite.GenValKeys(2 + (n / 3)) + for i := 0; i < n; i++ { + vals := keys.ToValidators(10, int64(n/2)) + h := int64(20 + 10*i) + fcs = append(fcs, keys.GenFullCommit(chainID, h, nil, vals, appHash, []byte("params"), []byte("results"), 0, 5)) + heights = append(heights, h) + } + return fcs, heights +}