github.com/jgbaldwinbrown/perf@v0.1.1/benchproc/filter.go (about)

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package benchproc
     6  
     7  import (
     8  	"fmt"
     9  
    10  	"golang.org/x/perf/benchfmt"
    11  	"golang.org/x/perf/benchproc/internal/parse"
    12  )
    13  
    14  // A Filter filters benchmarks and benchmark observations.
    15  type Filter struct {
    16  	// match is the filter function that implements this filter.
    17  	match filterFn
    18  }
    19  
    20  // filterFn is a filter function. If it matches individual measurements,
    21  // it returns a non-nil mask (and the bool result is ignored). If it
    22  // matches whole results, it returns a nil mask and a single boolean
    23  // match result.
    24  type filterFn func(res *benchfmt.Result) (mask, bool)
    25  
    26  // NewFilter constructs a result filter from a boolean filter
    27  // expression, such as ".name:Copy /size:4k". See "go doc
    28  // golang.org/x/perf/benchproc/syntax" for a description of filter
    29  // syntax.
    30  //
    31  // To create a filter that matches everything, pass "*" for query.
    32  func NewFilter(query string) (*Filter, error) {
    33  	q, err := parse.ParseFilter(query)
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  
    38  	// Recursively walk the filter expression, "compiling" it into
    39  	// a filterFn.
    40  	//
    41  	// We cache extractor functions since it's common to see the
    42  	// same key multiple times.
    43  	extractors := make(map[string]extractor)
    44  	var walk func(q parse.Filter) (filterFn, error)
    45  	walk = func(q parse.Filter) (filterFn, error) {
    46  		var err error
    47  		switch q := q.(type) {
    48  		case *parse.FilterOp:
    49  			subs := make([]filterFn, len(q.Exprs))
    50  			for i, sub := range q.Exprs {
    51  				subs[i], err = walk(sub)
    52  				if err != nil {
    53  					return nil, err
    54  				}
    55  			}
    56  			return filterOp(q.Op, subs), nil
    57  
    58  		case *parse.FilterMatch:
    59  			if q.Key == ".unit" {
    60  				return func(res *benchfmt.Result) (mask, bool) {
    61  					// Find the units this matches.
    62  					m := newMask(len(res.Values))
    63  					for i := range res.Values {
    64  						if q.MatchString(res.Values[i].Unit) || (res.Values[i].OrigUnit != "" && q.MatchString(res.Values[i].OrigUnit)) {
    65  							m.set(i)
    66  						}
    67  					}
    68  					return m, false
    69  				}, nil
    70  			}
    71  
    72  			if q.Key == ".config" {
    73  				return nil, &parse.SyntaxError{query, q.Off, ".config is only allowed in projections"}
    74  			}
    75  
    76  			// Construct the extractor.
    77  			ext := extractors[q.Key]
    78  			if ext == nil {
    79  				ext, err = newExtractor(q.Key)
    80  				if err != nil {
    81  					return nil, &parse.SyntaxError{query, q.Off, err.Error()}
    82  				}
    83  				extractors[q.Key] = ext
    84  			}
    85  
    86  			// Make the filter function.
    87  			return func(res *benchfmt.Result) (mask, bool) {
    88  				return nil, q.Match(ext(res))
    89  			}, nil
    90  		}
    91  		panic(fmt.Sprintf("unknown query node type %T", q))
    92  	}
    93  	f, err := walk(q)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	return &Filter{f}, nil
    98  }
    99  
   100  func filterOp(op parse.Op, subs []filterFn) filterFn {
   101  	switch op {
   102  	case parse.OpNot:
   103  		sub := subs[0]
   104  		return func(res *benchfmt.Result) (mask, bool) {
   105  			m, x := sub(res)
   106  			if m == nil {
   107  				return nil, !x
   108  			}
   109  			m.not()
   110  			return m, false
   111  		}
   112  
   113  	case parse.OpAnd:
   114  		return func(res *benchfmt.Result) (mask, bool) {
   115  			var m mask
   116  			for _, sub := range subs {
   117  				m2, x := sub(res)
   118  				if m2 == nil {
   119  					if !x {
   120  						// Short-circuit
   121  						return nil, false
   122  					}
   123  				} else if m == nil {
   124  					m = m2
   125  				} else {
   126  					m.and(m2)
   127  				}
   128  			}
   129  			return m, true
   130  		}
   131  
   132  	case parse.OpOr:
   133  		return func(res *benchfmt.Result) (mask, bool) {
   134  			var m mask
   135  			for _, sub := range subs {
   136  				m2, x := sub(res)
   137  				if m2 == nil {
   138  					if x {
   139  						// Short-circuit
   140  						return nil, true
   141  					}
   142  				} else if m == nil {
   143  					m = m2
   144  				} else {
   145  					m.or(m2)
   146  				}
   147  			}
   148  			return m, false
   149  		}
   150  	}
   151  	panic(fmt.Sprintf("unknown query op %v", op))
   152  }
   153  
   154  // Note: Right now the two methods below always return a nil error, but
   155  // the intent is to add more complicated types to projection expressions
   156  // (such as "commit") that may filter out results they can't parse with
   157  // an error (e.g., "unknown commit hash").
   158  
   159  // Apply rewrites res.Values to keep only the measurements that match
   160  // the Filter f and reports whether any measurements remain.
   161  //
   162  // Apply returns true if all or part of res.Values is kept by the filter.
   163  // Otherwise, it sets res.Values to an empty slice and returns false
   164  // to indicate res was completely filtered out.
   165  //
   166  // If Apply returns false, it may return a non-nil error
   167  // indicating why the result was filtered out.
   168  func (f *Filter) Apply(res *benchfmt.Result) (bool, error) {
   169  	m, err := f.Match(res)
   170  	return m.Apply(res), err
   171  }
   172  
   173  // Match returns the set of res.Values that match f.
   174  //
   175  // In contrast with the Apply method, this does not modify the Result.
   176  //
   177  // If the Match is empty, it may return a non-nil error
   178  // indicating why the result was filtered out.
   179  func (f *Filter) Match(res *benchfmt.Result) (Match, error) {
   180  	m, x := f.match(res)
   181  	return Match{len(res.Values), m, x}, nil
   182  }
   183  
   184  type mask []uint32
   185  
   186  func newMask(n int) mask {
   187  	return mask(make([]uint32, (n+31)/32))
   188  }
   189  
   190  func (m mask) set(i int) {
   191  	m[i/32] |= 1 << (i % 32)
   192  }
   193  
   194  func (m mask) and(n mask) {
   195  	for i := range m {
   196  		m[i] &= n[i]
   197  	}
   198  }
   199  
   200  func (m mask) or(n mask) {
   201  	for i := range m {
   202  		m[i] |= n[i]
   203  	}
   204  }
   205  
   206  func (m mask) not() {
   207  	for i := range m {
   208  		m[i] = ^m[i]
   209  	}
   210  }
   211  
   212  // A Match records the set of result measurements that matched a filter
   213  // query.
   214  type Match struct {
   215  	// n is the number of bits in this match.
   216  	n int
   217  
   218  	// m and x record the result of a filterFn. See filterFn for
   219  	// their meaning.
   220  	m mask
   221  	x bool
   222  }
   223  
   224  // All reports whether all measurements in a result matched the query.
   225  func (m *Match) All() bool {
   226  	if m.m == nil {
   227  		return m.x
   228  	}
   229  	for i, x := range m.m {
   230  		// Set all bits above m.n.
   231  		if x|(0xffffffff<<(m.n-i*32)) != 0xffffffff {
   232  			return false
   233  		}
   234  	}
   235  	return true
   236  }
   237  
   238  // Any reports whether any measurements in a result matched the query.
   239  func (m *Match) Any() bool {
   240  	if m.m == nil {
   241  		return m.x
   242  	}
   243  	for i, x := range m.m {
   244  		// Zero all bits above m.n.
   245  		if x&^(0xffffffff<<(m.n-i*32)) != 0 {
   246  			return true
   247  		}
   248  	}
   249  	return false
   250  }
   251  
   252  // Test reports whether measurement i of a result matched the query.
   253  func (m *Match) Test(i int) bool {
   254  	if i < 0 || i >= m.n {
   255  		return false
   256  	} else if m.m == nil {
   257  		return m.x
   258  	}
   259  	return m.m[i/32]&(1<<(i%32)) != 0
   260  }
   261  
   262  // Apply rewrites res.Values to keep only the measurements that match m.
   263  // It reports whether any Values remain.
   264  func (m *Match) Apply(res *benchfmt.Result) bool {
   265  	if m.All() {
   266  		return true
   267  	}
   268  	if !m.Any() {
   269  		res.Values = res.Values[:0]
   270  		return false
   271  	}
   272  
   273  	j := 0
   274  	for i, val := range res.Values {
   275  		if m.Test(i) {
   276  			res.Values[j] = val
   277  			j++
   278  		}
   279  	}
   280  	res.Values = res.Values[:j]
   281  	return j > 0
   282  }