// Copyright 2019-2022 Dolthub, Inc. // // 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 benchmark_runner import ( "errors" "path/filepath" "strconv" "strings" "github.com/google/uuid" ) var ( ErrUnableToParseOutput = errors.New("unable to parse output") ErrUnsupportedHeaderField = errors.New("unsupported header field") ) // Result stores the output from a sysbench test run type Result struct { // Id is the uuid of this result row Id string `json:"id"` // SuiteId is the test suite id this test result is associated with SuiteId string `json:"suite_id"` // TestId is the unique id for this test TestId string `json:"test_id"` // RuntimeOS is the runtime platform RuntimeOS string `json:"runtime_os"` // RuntimeGoArch is the runtime architecture RuntimeGoArch string `json:"runtime_goarch"` // ServerName is the name of the server used for the benchmark ServerName string `json:"server_name"` // ServerVersion is the server's version ServerVersion string `json:"server_version"` // ServerParams are the params used to run the server ServerParams string `json:"server_params"` // TestName is the name of the test TestName string `json:"test_name"` // TestParams are the params used to run the test TestParams string `json:"test_params"` // CreatedAt is the time the result was created UTC CreatedAt string `json:"created_at"` // SqlReadQueries is the number of read queries performed SqlReadQueries int64 `json:"sql_read_queries"` // SqlWriteQueries is the number of write queries performed SqlWriteQueries int64 `json:"sql_write_queries"` // SqlOtherQueries is the number of other queries performed SqlOtherQueries int64 `json:"sql_other_queries"` // SqlTotalQueries is the number of total queries performed SqlTotalQueries int64 `json:"sql_total_queries"` // SqlTotalQueriesPerSecond is the number of queries per second SqlTotalQueriesPerSecond float64 `json:"sql_total_queries_per_second"` // TransactionsTotal is the number of transactions performed TransactionsTotal int64 `json:"sql_transactions_total"` // TransactionsPerSecond is the number of transactions per second TransactionsPerSecond float64 `json:"sql_transactions_per_second"` // IgnoredErrorsTotal is the number of errors ignored IgnoredErrorsTotal int64 `json:"sql_ignored_errors_total"` // IgnoredErrorsPerSecond is the number of errors ignored per second IgnoredErrorsPerSecond float64 `json:"sql_ignored_errors_per_second"` // ReconnectsTotal is the number of reconnects performed ReconnectsTotal int64 `json:"sql_reconnects_total"` // ReconnectsPerSecond is the number of reconnects per second ReconnectsPerSecond float64 `json:"sql_reconnects_per_second"` // TotalTimeSeconds is the total time elapsed for this test TotalTimeSeconds float64 `json:"total_time_seconds"` // TotalNumberOfEvents is the total number of events TotalNumberOfEvents int64 `json:"total_number_of_events"` // LatencyMinMS is the minimum latency in milliseconds LatencyMinMS float64 `json:"latency_minimum_ms"` // LatencyAvgMS is the average latency in milliseconds LatencyAvgMS float64 `json:"latency_average_ms"` // LatencyMaxMS is the maximum latency in milliseconds LatencyMaxMS float64 `json:"latency_maximum_ms"` // LatencyPercentile is the latency of the 95th percentile LatencyPercentile float64 `json:"latency_percentile"` // LatencySumMS is the latency sum in milliseconds LatencySumMS float64 `json:"latency_sum_ms"` } // Results is a slice of Result type Results []*Result // Stamp timestamps the result using the provided stamp function func (r *Result) Stamp(stampFunc func() string) { if r.CreatedAt != "" || stampFunc == nil { return } r.CreatedAt = stampFunc() } func NewResult(server ServerType, version, testName, testId, suiteId, runtimeOs, runtimeGoArch string, serverParams, testParams []string, idFunc func() string, fromScript bool) *Result { var getId func() string if idFunc == nil { getId = func() string { return uuid.New().String() } } else { getId = idFunc } var name string if fromScript { base := filepath.Base(testName) ext := filepath.Ext(base) name = strings.TrimSuffix(base, ext) } else { name = testName } return &Result{ Id: getId(), SuiteId: suiteId, TestId: testId, RuntimeOS: runtimeOs, RuntimeGoArch: runtimeGoArch, ServerName: string(server), ServerVersion: version, ServerParams: strings.Join(serverParams, " "), TestName: name, TestParams: strings.Join(testParams, " "), } } // UpdateResult extracts the key and value from the given line and updates the given Result func UpdateResult(result *Result, line string) error { lineParts := strings.Split(line, ":") key := strings.TrimSpace(lineParts[0]) if len(lineParts) > 1 { rawVal := strings.TrimSpace(lineParts[1]) err := updateResult(result, key, rawVal) if err != nil { return err } } return nil } // updateResult updates a field in the given Result with the given value func updateResult(result *Result, key, val string) error { var k string if strings.Contains(key, percentile) { k = percentile } else { k = key } switch k { case read: i, err := fromStringInt64(val) if err != nil { return err } result.SqlReadQueries = i case write: i, err := fromStringInt64(val) if err != nil { return err } result.SqlWriteQueries = i case other: i, err := fromStringInt64(val) if err != nil { return err } result.SqlOtherQueries = i case totalQueries: i, err := fromStringInt64(val) if err != nil { return err } result.SqlTotalQueries = i case transactions: total, perSecond, err := FromValWithParens(val) if err != nil { return err } t, err := fromStringInt64(total) if err != nil { return err } p, err := fromStringFloat64(fromPerSecondVal(perSecond)) if err != nil { return err } result.TransactionsTotal = t result.TransactionsPerSecond = p case queriesPerSec: _, perSecond, err := FromValWithParens(val) if err != nil { return err } p, err := fromStringFloat64(fromPerSecondVal(perSecond)) if err != nil { return err } result.SqlTotalQueriesPerSecond = p case ignoredErrors: total, perSecond, err := FromValWithParens(val) if err != nil { return err } t, err := fromStringInt64(total) if err != nil { return err } p, err := fromStringFloat64(fromPerSecondVal(perSecond)) if err != nil { return err } result.IgnoredErrorsTotal = t result.IgnoredErrorsPerSecond = p case reconnects: total, perSecond, err := FromValWithParens(val) if err != nil { return err } t, err := fromStringInt64(total) if err != nil { return err } p, err := fromStringFloat64(fromPerSecondVal(perSecond)) if err != nil { return err } result.ReconnectsTotal = t result.ReconnectsPerSecond = p case totalTimeSecs: t, err := fromStringFloat64(fromSecondsVal(val)) if err != nil { return err } result.TotalTimeSeconds = t case totalEvents: i, err := fromStringInt64(val) if err != nil { return err } result.TotalNumberOfEvents = i case min: f, err := fromStringFloat64(val) if err != nil { return err } result.LatencyMinMS = f case avg: f, err := fromStringFloat64(val) if err != nil { return err } result.LatencyAvgMS = f case max: f, err := fromStringFloat64(val) if err != nil { return err } result.LatencyMaxMS = f case percentile: f, err := fromStringFloat64(val) if err != nil { return err } result.LatencyPercentile = f case sum: f, err := fromStringFloat64(val) if err != nil { return err } result.LatencySumMS = f default: return nil } return nil } func OutputToResult(output []byte, server ServerType, version, testName, testId, suiteId, runtimeOs, runtimeGoArch string, serverParams, testParams []string, idFunc func() string, fromScript bool) (*Result, error) { result := NewResult(server, version, testName, testId, suiteId, runtimeOs, runtimeGoArch, serverParams, testParams, idFunc, fromScript) lines := strings.Split(string(output), "\n") var process bool for _, l := range lines { trimmed := strings.TrimSpace(l) if trimmed == "" { continue } if strings.HasPrefix(trimmed, SqlStatsPrefix) { process = true continue } if process { err := UpdateResult(result, trimmed) if err != nil { return result, err } } } return result, nil } // FromValWithParens takes a string containing parens and // returns the value outside the parens first, and the value // inside the parens second func FromValWithParens(val string) (string, string, error) { if val == "" { return "", "", nil } parts := strings.Split(val, "(") if len(parts) <= 1 { return strings.TrimSpace(parts[0]), "", nil } if len(parts) > 2 { return "", "", ErrUnableToParseOutput } return strings.TrimSpace(parts[0]), strings.Trim(parts[1], ")"), nil } func fromPerSecondVal(val string) string { return strings.TrimSuffix(val, " per sec.") } func fromSecondsVal(val string) string { return strings.TrimRight(val, "s") } func fromStringInt64(val string) (int64, error) { return strconv.ParseInt(val, 10, 64) } func fromStringFloat64(val string) (float64, error) { return strconv.ParseFloat(val, 64) }