cmd/benchfilter: new command for filtering and tidying benchmark results

Change-Id: I66dac0c90f27f932abfabee0e86a216606d981d1
Reviewed-on: https://go-review.googlesource.com/c/perf/+/283618
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Michael Pratt <mpratt@google.com>
Run-TryBot: Austin Clements <austin@google.com>
Reviewed-by: David Chase <drchase@google.com>
diff --git a/README.md b/README.md
index 5a744c3..091947d 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,9 @@
 [cmd/benchstat](cmd/benchstat) computes statistical summaries and A/B
 comparisons of Go benchmarks.
 
+[cmd/benchfilter](cmd/benchfilter) filters the contents of benchmark
+result files.
+
 [cmd/benchsave](cmd/benchsave) publishes benchmark results to
 [perf.golang.org](https://perf.golang.org).
 
diff --git a/cmd/benchfilter/main.go b/cmd/benchfilter/main.go
new file mode 100644
index 0000000..5ede808
--- /dev/null
+++ b/cmd/benchfilter/main.go
@@ -0,0 +1,82 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// benchfilter reads Go benchmark results from input files, filters
+// them, and writes filtered benchmark results to stdout. If no inputs
+// are provided, it reads from stdin.
+//
+// The filter language is described at
+// https://pkg.go.dev/golang.org/x/perf/cmd/benchstat#Filtering
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+
+	"golang.org/x/perf/benchfmt"
+	"golang.org/x/perf/benchproc"
+)
+
+func usage() {
+	fmt.Fprintf(flag.CommandLine.Output(), `Usage: benchfilter query [inputs...]
+
+benchfilter reads Go benchmark results from input files, filters them,
+and writes filtered benchmark results to stdout. If no inputs are
+provided, it reads from stdin.
+
+The filter language is described at
+https://pkg.go.dev/golang.org/x/perf/cmd/benchstat#Filtering
+`)
+	flag.PrintDefaults()
+}
+
+func main() {
+	log.SetPrefix("")
+	log.SetFlags(0)
+
+	flag.Usage = usage
+	flag.Parse()
+	if flag.NArg() < 1 {
+		usage()
+		os.Exit(2)
+	}
+
+	// TODO: Consider adding filtering on values, like "@ns/op>=100".
+
+	filter, err := benchproc.NewFilter(flag.Arg(0))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	writer := benchfmt.NewWriter(os.Stdout)
+	files := benchfmt.Files{Paths: flag.Args()[1:], AllowStdin: true, AllowLabels: true}
+	for files.Scan() {
+		rec := files.Result()
+		switch rec := rec.(type) {
+		case *benchfmt.SyntaxError:
+			// Non-fatal result parse error. Warn
+			// but keep going.
+			fmt.Fprintln(os.Stderr, rec)
+			continue
+		case *benchfmt.Result:
+			if ok, err := filter.Apply(rec); !ok {
+				if err != nil {
+					// Print the reason we rejected this result.
+					fmt.Fprintln(os.Stderr, err)
+				}
+				continue
+			}
+		}
+
+		err = writer.Write(rec)
+		if err != nil {
+			log.Fatal("writing output: ", err)
+		}
+	}
+	if err := files.Err(); err != nil {
+		log.Fatal(err)
+	}
+}