Introduce Golang-based benchmarking utility (#769)

* Introduce Golang-based benchmarking utility

* benchmark fio: properly configure logger level

* benchmark: properly terminate on Ctrl+C when initializing/running Tart

* benchmark(fio): increase runtime to 30 seconds

* benchmark(fio): IOPS are already per second

* benchmark(fio): --numjobs 1 --iodepth 1 --end_fsync 1

* benchmark(README.md): add results
This commit is contained in:
Nikolay Edigaryev
2024-03-27 18:45:01 +04:00
committed by GitHub
parent 1a3b862631
commit 5cd83c38cd
16 changed files with 694 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
package executor
import (
"context"
)
type Executor interface {
Name() string
Run(ctx context.Context, command string) ([]byte, error)
Close() error
}
@@ -0,0 +1,42 @@
package local
import (
"bytes"
"context"
"go.uber.org/zap"
"go.uber.org/zap/zapio"
"io"
"os/exec"
)
type Local struct {
logger *zap.Logger
}
func New(logger *zap.Logger) (*Local, error) {
return &Local{
logger: logger,
}, nil
}
func (local *Local) Name() string {
return "local"
}
func (local *Local) Run(ctx context.Context, command string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "zsh", "-c", command)
loggerWriter := &zapio.Writer{Log: local.logger, Level: zap.DebugLevel}
stdoutBuf := &bytes.Buffer{}
cmd.Stdout = io.MultiWriter(stdoutBuf, loggerWriter)
cmd.Stderr = loggerWriter
err := cmd.Run()
return stdoutBuf.Bytes(), err
}
func (local *Local) Close() error {
return nil
}
@@ -0,0 +1,20 @@
package local_test
import (
"context"
"github.com/cirruslabs/tart/benchmark/internal/executor/local"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"testing"
)
func TestLocal(t *testing.T) {
local, err := local.New(zap.NewNop())
require.NoError(t, err)
output, err := local.Run(context.Background(), "echo \"this is a test\"")
require.NoError(t, err)
require.Equal(t, "this is a test\n", string(output))
require.NoError(t, local.Close())
}
+35
View File
@@ -0,0 +1,35 @@
package tart
import (
"bytes"
"context"
"go.uber.org/zap"
"go.uber.org/zap/zapio"
"io"
"os/exec"
"strings"
)
const tartBinaryName = "tart"
func Cmd(ctx context.Context, logger *zap.Logger, args ...string) error {
_, err := CmdWithOutput(ctx, logger, args...)
return err
}
func CmdWithOutput(ctx context.Context, logger *zap.Logger, args ...string) (string, error) {
logger.Sugar().Debugf("running %s %s", tartBinaryName, strings.Join(args, " "))
cmd := exec.CommandContext(ctx, tartBinaryName, args...)
loggerWriter := &zapio.Writer{Log: logger, Level: zap.DebugLevel}
stdoutBuf := &bytes.Buffer{}
cmd.Stdout = io.MultiWriter(stdoutBuf, loggerWriter)
cmd.Stderr = loggerWriter
err := cmd.Run()
return stdoutBuf.String(), err
}
+133
View File
@@ -0,0 +1,133 @@
package tart
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/avast/retry-go/v4"
"github.com/google/uuid"
"go.uber.org/zap"
"golang.org/x/crypto/ssh"
"net"
"strings"
"time"
)
const baseImage = "ghcr.io/cirruslabs/macos-sonoma-base:latest"
type Tart struct {
vmRunCancel context.CancelFunc
vmName string
sshClient *ssh.Client
logger *zap.Logger
}
func New(ctx context.Context, logger *zap.Logger) (*Tart, error) {
tart := &Tart{
vmName: fmt.Sprintf("tart-benchmark-%s", uuid.NewString()),
logger: logger,
}
if err := Cmd(ctx, tart.logger, "pull", baseImage); err != nil {
return nil, err
}
if err := Cmd(ctx, tart.logger, "clone", baseImage, tart.vmName); err != nil {
return nil, err
}
vmRunCtx, vmRunCancel := context.WithCancel(ctx)
tart.vmRunCancel = vmRunCancel
go func() {
_ = Cmd(vmRunCtx, tart.logger, "run", "--no-graphics", tart.vmName)
}()
ip, err := CmdWithOutput(ctx, tart.logger, "ip", "--wait", "60", tart.vmName)
if err != nil {
return nil, tart.Close()
}
err = retry.Do(func() error {
dialer := net.Dialer{
Timeout: 1 * time.Second,
}
addr := fmt.Sprintf("%s:22", strings.TrimSpace(ip))
netConn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return err
}
sshConn, chans, reqs, err := ssh.NewClientConn(netConn, addr, &ssh.ClientConfig{
User: "admin",
Auth: []ssh.AuthMethod{
ssh.Password("admin"),
},
HostKeyCallback: func(_ string, _ net.Addr, _ ssh.PublicKey) error {
return nil
},
})
if err != nil {
return err
}
tart.sshClient = ssh.NewClient(sshConn, chans, reqs)
return nil
}, retry.RetryIf(func(err error) bool {
return !errors.Is(err, context.Canceled)
}))
if err != nil {
return nil, tart.Close()
}
return tart, nil
}
func (tart *Tart) Name() string {
return "Tart"
}
func (tart *Tart) Run(ctx context.Context, command string) ([]byte, error) {
sshSession, err := tart.sshClient.NewSession()
if err != nil {
return nil, err
}
// Work around x/crypto/ssh not being context.Context-friendly (e.g. https://github.com/golang/go/issues/20288)
monitorCtx, monitorCancel := context.WithCancel(ctx)
go func() {
<-monitorCtx.Done()
_ = sshSession.Close()
}()
defer monitorCancel()
stdoutBuf := &bytes.Buffer{}
sshSession.Stdin = bytes.NewBufferString(command)
sshSession.Stdout = stdoutBuf
if err := sshSession.Shell(); err != nil {
return nil, err
}
if err := sshSession.Wait(); err != nil {
return nil, err
}
return stdoutBuf.Bytes(), nil
}
func (tart *Tart) Close() error {
if tart.sshClient != nil {
_ = tart.sshClient.Close()
}
tart.vmRunCancel()
_ = Cmd(context.Background(), tart.logger, "delete", tart.vmName)
return nil
}
@@ -0,0 +1,22 @@
package tart_test
import (
"context"
"github.com/cirruslabs/tart/benchmark/internal/executor/tart"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"testing"
)
func TestTart(t *testing.T) {
ctx := context.Background()
tart, err := tart.New(ctx, zap.NewNop())
require.NoError(t, err)
output, err := tart.Run(ctx, "echo \"this is a test\"")
require.NoError(t, err)
require.Equal(t, "this is a test\n", string(output))
require.NoError(t, tart.Close())
}