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 }