Files
dolt/go/performance/utils/sysbench_runner/results.go
T
2022-03-10 10:20:40 -08:00

400 lines
10 KiB
Go

// 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 sysbench_runner
import (
"errors"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
const (
stampFormat = time.RFC3339
SqlStatsPrefix = "SQL statistics:"
read = "read"
write = "write"
other = "other"
totalQueries = "total"
totalEvents = "total number of events"
min = "min"
avg = "avg"
max = "max"
percentile = "percentile"
sum = "sum"
transactions = "transactions"
queriesPerSec = "queries"
ignoredErrors = "ignored errors"
reconnects = "reconnects"
totalTimeSecs = "total time"
)
var (
ResultFileTemplate = "%s_%s_%s_sysbench_performance%s"
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()
}
// FromConfigsNewResult returns a new result with some fields set based on the provided configs
func FromConfigsNewResult(config *Config, serverConfig *ServerConfig, t *Test, suiteId string, idFunc func() string) (*Result, error) {
serverParams := serverConfig.GetServerArgs()
var getId func() string
if idFunc == nil {
getId = func() string {
return uuid.New().String()
}
} else {
getId = idFunc
}
var name string
if t.FromScript {
base := filepath.Base(t.Name)
ext := filepath.Ext(base)
name = strings.TrimSuffix(base, ext)
} else {
name = t.Name
}
return &Result{
Id: getId(),
SuiteId: suiteId,
TestId: t.id,
RuntimeOS: config.RuntimeOS,
RuntimeGoArch: config.RuntimeGoArch,
ServerName: string(serverConfig.Server),
ServerVersion: serverConfig.Version,
ServerParams: strings.Join(serverParams, " "),
TestName: name,
TestParams: strings.Join(t.Params, " "),
}, nil
}
// FromOutputResult accepts raw sysbench run output and returns the Result
func FromOutputResult(output []byte, config *Config, serverConfig *ServerConfig, test *Test, suiteId string, idFunc func() string) (*Result, error) {
result, err := FromConfigsNewResult(config, serverConfig, test, suiteId, idFunc)
if err != nil {
return nil, err
}
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
}
// 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
}
// 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)
}