summaryrefslogtreecommitdiffhomepage
path: root/tools/parsers
diff options
context:
space:
mode:
Diffstat (limited to 'tools/parsers')
-rw-r--r--tools/parsers/BUILD27
-rw-r--r--tools/parsers/go_parser.go151
-rw-r--r--tools/parsers/go_parser_test.go171
3 files changed, 349 insertions, 0 deletions
diff --git a/tools/parsers/BUILD b/tools/parsers/BUILD
new file mode 100644
index 000000000..7d9c9a3fb
--- /dev/null
+++ b/tools/parsers/BUILD
@@ -0,0 +1,27 @@
+load("//tools:defs.bzl", "go_library", "go_test")
+
+package(licenses = ["notice"])
+
+go_test(
+ name = "parsers_test",
+ size = "small",
+ srcs = ["go_parser_test.go"],
+ library = ":parsers",
+ deps = [
+ "//tools/bigquery",
+ "@com_github_google_go_cmp//cmp:go_default_library",
+ ],
+)
+
+go_library(
+ name = "parsers",
+ testonly = 1,
+ srcs = [
+ "go_parser.go",
+ ],
+ visibility = ["//:sandbox"],
+ deps = [
+ "//test/benchmarks/tools",
+ "//tools/bigquery",
+ ],
+)
diff --git a/tools/parsers/go_parser.go b/tools/parsers/go_parser.go
new file mode 100644
index 000000000..2cf74c883
--- /dev/null
+++ b/tools/parsers/go_parser.go
@@ -0,0 +1,151 @@
+// Copyright 2020 The gVisor Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package parsers holds parsers to parse Benchmark test output.
+//
+// Parsers parse Benchmark test output and place it in BigQuery
+// structs for sending to BigQuery databases.
+package parsers
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "gvisor.dev/gvisor/test/benchmarks/tools"
+ "gvisor.dev/gvisor/tools/bigquery"
+)
+
+// parseOutput expects golang benchmark output returns a Benchmark struct formatted for BigQuery.
+func parseOutput(output string, metadata *bigquery.Metadata, official bool) ([]*bigquery.Benchmark, error) {
+ var benchmarks []*bigquery.Benchmark
+ lines := strings.Split(output, "\n")
+ for _, line := range lines {
+ bm, err := parseLine(line, metadata, official)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse line '%s': %v", line, err)
+ }
+ if bm != nil {
+ benchmarks = append(benchmarks, bm)
+ }
+ }
+ return benchmarks, nil
+}
+
+// parseLine handles parsing a benchmark line into a bigquery.Benchmark.
+//
+// Example: "BenchmarkRuby/server_threads.1-6 1 1397875880 ns/op 140 requests_per_second.QPS"
+//
+// This function will return the following benchmark:
+// *bigquery.Benchmark{
+// Name: BenchmarkRuby
+// []*bigquery.Condition{
+// {Name: GOMAXPROCS, 6}
+// {Name: server_threads, 1}
+// }
+// []*bigquery.Metric{
+// {Name: ns/op, Unit: ns/op, Sample: 1397875880}
+// {Name: requests_per_second, Unit: QPS, Sample: 140 }
+// }
+// Metadata: metadata
+//}
+func parseLine(line string, metadata *bigquery.Metadata, official bool) (*bigquery.Benchmark, error) {
+ fields := strings.Fields(line)
+
+ // Check if this line is a Benchmark line. Otherwise ignore the line.
+ if len(fields) < 2 || !strings.HasPrefix(fields[0], "Benchmark") {
+ return nil, nil
+ }
+
+ iters, err := strconv.Atoi(fields[1])
+ if err != nil {
+ return nil, fmt.Errorf("expecting number of runs, got %s: %v", fields[1], err)
+ }
+
+ name, params, err := parseNameParams(fields[0])
+ if err != nil {
+ return nil, fmt.Errorf("parse name/params: %v", err)
+ }
+
+ bm := bigquery.NewBenchmark(name, iters, official)
+ bm.Metadata = metadata
+ for _, p := range params {
+ bm.AddCondition(p.Name, p.Value)
+ }
+
+ for i := 1; i < len(fields)/2; i++ {
+ value := fields[2*i]
+ metric := fields[2*i+1]
+ if err := makeMetric(bm, value, metric); err != nil {
+ return nil, fmt.Errorf("makeMetric on metric %q value: %s: %v", metric, value, err)
+ }
+ }
+ return bm, nil
+}
+
+// parseNameParams parses the Name, GOMAXPROCS, and Params from the test.
+// Field here should be of the format TESTNAME/PARAMS-GOMAXPROCS.
+// Parameters will be separated by a "/" with individual params being
+// "name.value".
+func parseNameParams(field string) (string, []*tools.Parameter, error) {
+ var params []*tools.Parameter
+ // Remove GOMAXPROCS from end.
+ maxIndex := strings.LastIndex(field, "-")
+ if maxIndex < 0 {
+ return "", nil, fmt.Errorf("GOMAXPROCS not found: %s", field)
+ }
+ maxProcs := field[maxIndex+1:]
+ params = append(params, &tools.Parameter{
+ Name: "GOMAXPROCS",
+ Value: maxProcs,
+ })
+
+ remainder := field[0:maxIndex]
+ index := strings.Index(remainder, "/")
+ if index == -1 {
+ return remainder, params, nil
+ }
+
+ name := remainder[0:index]
+ p := remainder[index+1:]
+
+ ps, err := tools.NameToParameters(p)
+ if err != nil {
+ return "", nil, fmt.Errorf("NameToParameters %s: %v", field, err)
+ }
+ params = append(params, ps...)
+ return name, params, nil
+}
+
+// makeMetric parses metrics and adds them to the passed Benchmark.
+func makeMetric(bm *bigquery.Benchmark, value, metric string) error {
+ switch metric {
+ // Ignore most output from golang benchmarks.
+ case "MB/s", "B/op", "allocs/op":
+ return nil
+ case "ns/op":
+ val, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return fmt.Errorf("ParseFloat %s: %v", value, err)
+ }
+ bm.AddMetric(metric /*metric name*/, metric /*unit*/, val /*sample*/)
+ default:
+ m, err := tools.ParseCustomMetric(value, metric)
+ if err != nil {
+ return fmt.Errorf("ParseCustomMetric %s: %v ", metric, err)
+ }
+ bm.AddMetric(m.Name, m.Unit, m.Sample)
+ }
+ return nil
+}
diff --git a/tools/parsers/go_parser_test.go b/tools/parsers/go_parser_test.go
new file mode 100644
index 000000000..36996b7c8
--- /dev/null
+++ b/tools/parsers/go_parser_test.go
@@ -0,0 +1,171 @@
+// Copyright 2020 The gVisor Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package parsers
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "gvisor.dev/gvisor/tools/bigquery"
+)
+
+func TestParseLine(t *testing.T) {
+ testCases := []struct {
+ name string
+ data string
+ want *bigquery.Benchmark
+ }{
+ {
+ name: "Iperf",
+ data: "BenchmarkIperf/Upload-6 1 11094914892 ns/op 4751711232 bandwidth.bytes_per_second",
+ want: &bigquery.Benchmark{
+ Name: "BenchmarkIperf",
+ Condition: []*bigquery.Condition{
+ {
+ Name: "GOMAXPROCS",
+ Value: "6",
+ },
+ {
+ Name: "Upload",
+ Value: "Upload",
+ },
+ },
+ Metric: []*bigquery.Metric{
+ {
+ Name: "ns/op",
+ Unit: "ns/op",
+ Sample: 11094914892.0,
+ },
+ {
+ Name: "bandwidth",
+ Unit: "bytes_per_second",
+ Sample: 4751711232.0,
+ },
+ },
+ },
+ },
+ {
+ name: "Ruby",
+ data: "BenchmarkRuby/server_threads.1-6 1 1397875880 ns/op 0.00710 average_latency.s 140 requests_per_second.QPS",
+ want: &bigquery.Benchmark{
+ Name: "BenchmarkRuby",
+ Condition: []*bigquery.Condition{
+ {
+ Name: "GOMAXPROCS",
+ Value: "6",
+ },
+ {
+ Name: "server_threads",
+ Value: "1",
+ },
+ },
+ Metric: []*bigquery.Metric{
+ {
+ Name: "ns/op",
+ Unit: "ns/op",
+ Sample: 1397875880.0,
+ },
+ {
+ Name: "average_latency",
+ Unit: "s",
+ Sample: 0.00710,
+ },
+ {
+ Name: "requests_per_second",
+ Unit: "QPS",
+ Sample: 140.0,
+ },
+ },
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := parseLine(tc.data, nil, false)
+ if err != nil {
+ t.Fatalf("parseLine failed with: %v", err)
+ }
+
+ tc.want.Timestamp = got.Timestamp
+
+ if !cmp.Equal(tc.want, got, nil) {
+ for _, c := range got.Condition {
+ t.Logf("Cond: %+v", c)
+ }
+ for _, m := range got.Metric {
+ t.Logf("Metric: %+v", m)
+ }
+ t.Fatalf("Compare failed want: %+v got: %+v", tc.want, got)
+ }
+ })
+
+ }
+}
+
+func TestParseOutput(t *testing.T) {
+ testCases := []struct {
+ name string
+ data string
+ numBenchmarks int
+ numMetrics int
+ numConditions int
+ }{
+ {
+ name: "Startup",
+ data: `
+ BenchmarkStartupEmpty
+ BenchmarkStartupEmpty-6 2 766377884 ns/op 1 allocs/op
+ BenchmarkStartupNode
+ BenchmarkStartupNode-6 1 1752158409 ns/op 1 allocs/op
+ `,
+ numBenchmarks: 2,
+ numMetrics: 1,
+ numConditions: 1,
+ },
+ {
+ name: "Ruby",
+ data: `BenchmarkRuby
+BenchmarkRuby/server_threads.1
+BenchmarkRuby/server_threads.1-6 1 1397875880 ns/op 0.00710 average_latency.s 140 requests_per_second.QPS
+BenchmarkRuby/server_threads.5
+BenchmarkRuby/server_threads.5-6 1 1416003331 ns/op 0.00950 average_latency.s 465 requests_per_second.QPS`,
+ numBenchmarks: 2,
+ numMetrics: 3,
+ numConditions: 2,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ bms, err := parseOutput(tc.data, nil, false)
+ if err != nil {
+ t.Fatalf("parseOutput failed: %v", err)
+ } else if len(bms) != tc.numBenchmarks {
+ t.Fatalf("NumBenchmarks failed want: %d got: %d %+v", tc.numBenchmarks, len(bms), bms)
+ }
+
+ for _, bm := range bms {
+ if len(bm.Metric) != tc.numMetrics {
+ t.Fatalf("NumMetrics failed want: %d got: %d %+v", tc.numMetrics, len(bm.Metric), bm.Metric)
+ }
+
+ if len(bm.Condition) != tc.numConditions {
+ t.Fatalf("NumConditions failed want: %d got: %d %+v", tc.numConditions, len(bm.Condition), bm.Condition)
+ }
+ }
+ })
+ }
+}