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  }