github.com/ethereum/go-ethereum@v1.16.1/cmd/workload/filtertestperf.go (about) 1 // Copyright 2025 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package main 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "math/rand" 23 "os" 24 "slices" 25 "sort" 26 "time" 27 28 "github.com/urfave/cli/v2" 29 ) 30 31 var ( 32 filterPerfCommand = &cli.Command{ 33 Name: "filterperf", 34 Usage: "Runs log filter performance test against an RPC endpoint", 35 ArgsUsage: "<RPC endpoint URL>", 36 Action: filterPerfCmd, 37 Flags: []cli.Flag{ 38 testSepoliaFlag, 39 testMainnetFlag, 40 filterQueryFileFlag, 41 filterErrorFileFlag, 42 }, 43 } 44 ) 45 46 const passCount = 3 47 48 func filterPerfCmd(ctx *cli.Context) error { 49 cfg := testConfigFromCLI(ctx) 50 f := newFilterTestSuite(cfg) 51 52 type queryTest struct { 53 query *filterQuery 54 bucket, index int 55 runtime []time.Duration 56 medianTime time.Duration 57 } 58 var queries, processed []queryTest 59 for i, bucket := range f.queries[:] { 60 for j, query := range bucket { 61 queries = append(queries, queryTest{query: query, bucket: i, index: j}) 62 } 63 } 64 65 // Run test queries. 66 var ( 67 failed, pruned, mismatch int 68 errors []*filterQuery 69 ) 70 for i := 1; i <= passCount; i++ { 71 fmt.Println("Performance test pass", i, "/", passCount) 72 for len(queries) > 0 { 73 pick := rand.Intn(len(queries)) 74 qt := queries[pick] 75 queries[pick] = queries[len(queries)-1] 76 queries = queries[:len(queries)-1] 77 start := time.Now() 78 qt.query.run(cfg.client, cfg.historyPruneBlock) 79 if qt.query.Err == errPrunedHistory { 80 pruned++ 81 continue 82 } 83 qt.runtime = append(qt.runtime, time.Since(start)) 84 slices.Sort(qt.runtime) 85 qt.medianTime = qt.runtime[len(qt.runtime)/2] 86 if qt.query.Err != nil { 87 qt.query.printError() 88 errors = append(errors, qt.query) 89 failed++ 90 continue 91 } 92 if rhash := qt.query.calculateHash(); *qt.query.ResultHash != rhash { 93 fmt.Printf("Filter query result mismatch: fromBlock: %d toBlock: %d addresses: %v topics: %v expected hash: %064x calculated hash: %064x\n", qt.query.FromBlock, qt.query.ToBlock, qt.query.Address, qt.query.Topics, *qt.query.ResultHash, rhash) 94 errors = append(errors, qt.query) 95 mismatch++ 96 continue 97 } 98 processed = append(processed, qt) 99 if len(processed)%50 == 0 { 100 fmt.Println(" processed:", len(processed), "remaining", len(queries), "failed:", failed, "pruned:", pruned, "result mismatch:", mismatch) 101 } 102 } 103 queries, processed = processed, nil 104 } 105 106 // Show results and stats. 107 fmt.Println("Performance test finished; processed:", len(queries), "failed:", failed, "pruned:", pruned, "result mismatch:", mismatch) 108 stats := make([]bucketStats, len(f.queries)) 109 var wildcardStats bucketStats 110 for _, qt := range queries { 111 bs := &stats[qt.bucket] 112 if qt.query.isWildcard() { 113 bs = &wildcardStats 114 } 115 bs.blocks += qt.query.ToBlock + 1 - qt.query.FromBlock 116 bs.count++ 117 bs.logs += len(qt.query.results) 118 bs.runtime += qt.medianTime 119 } 120 121 fmt.Println() 122 for i := range stats { 123 stats[i].print(fmt.Sprintf("bucket #%d", i+1)) 124 } 125 wildcardStats.print("wild card queries") 126 fmt.Println() 127 sort.Slice(queries, func(i, j int) bool { 128 return queries[i].medianTime > queries[j].medianTime 129 }) 130 for i, q := range queries { 131 if i >= 10 { 132 break 133 } 134 fmt.Printf("Most expensive query #%-2d median runtime: %13v max runtime: %13v result count: %4d fromBlock: %9d toBlock: %9d addresses: %v topics: %v\n", 135 i+1, q.medianTime, q.runtime[len(q.runtime)-1], len(q.query.results), q.query.FromBlock, q.query.ToBlock, q.query.Address, q.query.Topics) 136 } 137 writeErrors(ctx.String(filterErrorFileFlag.Name), errors) 138 return nil 139 } 140 141 type bucketStats struct { 142 blocks int64 143 count, logs int 144 runtime time.Duration 145 } 146 147 func (st *bucketStats) print(name string) { 148 if st.count == 0 { 149 return 150 } 151 fmt.Printf("%-20s queries: %4d average block length: %12.2f average log count: %7.2f average runtime: %13v\n", 152 name, st.count, float64(st.blocks)/float64(st.count), float64(st.logs)/float64(st.count), st.runtime/time.Duration(st.count)) 153 } 154 155 // writeQueries serializes the generated errors to the error file. 156 func writeErrors(errorFile string, errors []*filterQuery) { 157 file, err := os.Create(errorFile) 158 if err != nil { 159 exit(fmt.Errorf("Error creating filter error file %s: %v", errorFile, err)) 160 return 161 } 162 defer file.Close() 163 json.NewEncoder(file).Encode(errors) 164 }