From 0027fb5772290cffeebde74e3519ffcf0fca1d1b Mon Sep 17 00:00:00 2001 From: Mikhail Iudin Date: Sun, 22 Sep 2024 17:20:22 +0200 Subject: [PATCH] GO-3985 Add perftests for staging and local-only mode AccountCreate AccountSelectHot WorkspaceOpen WorkspaceCreate --- .github/staging_fake.yml | 30 ++ .github/workflows/perftests.yml | 37 +- .run/Run.run.xml | 14 +- Makefile | 2 +- cmd/perfstand/account_create/main.go | 110 ++++++ cmd/perfstand/account_select/main.go | 103 ++++++ cmd/perfstand/internal/util.go | 512 +++++++++++++++++++++++++++ 7 files changed, 788 insertions(+), 20 deletions(-) create mode 100644 .github/staging_fake.yml create mode 100644 cmd/perfstand/account_create/main.go create mode 100644 cmd/perfstand/account_select/main.go create mode 100644 cmd/perfstand/internal/util.go diff --git a/.github/staging_fake.yml b/.github/staging_fake.yml new file mode 100644 index 000000000..e1a9e7072 --- /dev/null +++ b/.github/staging_fake.yml @@ -0,0 +1,30 @@ +networkId: N9DU6hLkTAbvcpji3TCKPPd3UQWKGyzUxGmgJEyvhByqAjfD +nodes: + - peerId: FAKE_PEER_ID1 + addresses: + - lol1-any-sync-node1.somefakesite.com:6666 + - lol1-any-sync-node1.somefakesite.com:6667 + - quic://lol1.somefakesite.com:8888 + types: + - tree + - peerId: FAKE_PEER_ID2 + addresses: + - lol1-file13.somefakesite.com:6666 + - lol1-file13.somefakesite.com:6667 + - quic://lol1-file13.somefakesite.com:8888 + types: + - file + - peerId: FAKE_PEER_ID3 + addresses: + - lol1-coord1.somefakesite.com:6666 + - lol1-coord1.somefakesite.com:6667 + - quic://lol1-coord1.somefakesite.com:8888 + types: + - coordinator + - peerId: FAKE_PEER_ID4 + addresses: + - lol1-cons1.somefakesite.com:6666 + - lol1-cons1.somefakesite.com:6667 + - quic://lol1-cons1.somefakesite.com:8888 + types: + - consensus diff --git a/.github/workflows/perftests.yml b/.github/workflows/perftests.yml index 69bba967e..4a05ca6ba 100644 --- a/.github/workflows/perftests.yml +++ b/.github/workflows/perftests.yml @@ -1,3 +1,4 @@ +#https://linear.app/anytype/issue/GO-3985/make-performance-report-on-the-stand on: workflow_dispatch: inputs: @@ -8,7 +9,7 @@ on: perf-test: description: 'Run perf test times' required: true - default: '0' + default: '100' schedule: - cron: '0 0 * * *' # every day at midnight filters: @@ -22,7 +23,7 @@ permissions: contents: 'write' -name: Build +name: Perf tests jobs: build: runs-on: 'ARM64' @@ -35,13 +36,6 @@ jobs: echo $(go env GOPATH)/bin >> $GITHUB_PATH - name: Checkout uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ matrix.go-version }}- - name: Set env vars env: UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }} @@ -83,19 +77,26 @@ jobs: if [[ "${{ github.event_name }}" == "schedule" ]]; then RUN_COUNT=100 fi + echo $PERF_CONFIG_STAGING > config.json + mv .github/staging_fake.yml staging_fake.yml + PERF_CONFIG=$PWD/config.json cd cmd/perfstand/account_create - CGO_ENABLED="1" go run main.go $RUN_COUNT + CGO_ENABLED="1" go run main.go $PERF_CONFIG $RUN_COUNT cd ../account_select - CGO_ENABLED="1" go run main.go $RUN_COUNT + CGO_ENABLED="1" go run main.go $PERF_CONFIG $RUN_COUNT + cd ../../.. + echo $PERF_CONFIG_LOCAL > ./config.json + cd cmd/perfstand/account_create + CGO_ENABLED="1" go run main.go $PERF_CONFIG $RUN_COUNT + cd ../account_select + CGO_ENABLED="1" go run main.go $PERF_CONFIG $RUN_COUNT env: - TEST_MNEMONIC: ${{ secrets.TEST_MNEMONIC_30000 }} - CH_API_KEY: ${{ secrets.CH_API_KEY }} - ACCOUNT_HASH: ${{ secrets.ACCOUNT_HASH_30000 }} - ACCOUNT_SPACE: ${{ secrets.ACCOUNT_SPACE_30000 }} - ROOT_FOLDER: ${{ secrets.ROOT_FOLDER }} + PERF_CONFIG_STAGING: ${{ secrets.PERF_CONFIG_STAGING }} + PERF_CONFIG_LOCAL: ${{ secrets.PERF_CONFIG_LOCAL }} + CH_API_KEY: ${{ secrets.CH_PERF_API_KEY }} - name: Archive perf tests results uses: actions/upload-artifact@v4 with: - name: pprofs + name: traces path: | - *.pprof \ No newline at end of file + *.log \ No newline at end of file diff --git a/.run/Run.run.xml b/.run/Run.run.xml index bb5187a59..0595d2c20 100644 --- a/.run/Run.run.xml +++ b/.run/Run.run.xml @@ -2,14 +2,26 @@ - + + + + + diff --git a/Makefile b/Makefile index d253e4d40..0cfd5a3dd 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,7 @@ protos-java: build-server: setup-network-config @echo 'Building anytype-heart server...' @$(eval FLAGS += $$(shell govvv -flags -pkg github.com/anyproto/anytype-heart/util/vcs)) - @$(eval TAGS := nosigar nowatchdog) + @$(eval TAGS := $(TAGS) nosigar nowatchdog) ifdef ANY_SYNC_NETWORK @$(eval TAGS := $(TAGS) envnetworkcustom) endif diff --git a/cmd/perfstand/account_create/main.go b/cmd/perfstand/account_create/main.go new file mode 100644 index 000000000..c82b4069e --- /dev/null +++ b/cmd/perfstand/account_create/main.go @@ -0,0 +1,110 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + + "go.uber.org/atomic" + + "github.com/anyproto/anytype-heart/cmd/perfstand/internal" +) + +const AccountCreate = "AccountCreate" + +type input struct { + *internal.BasicInput +} + +type wallet struct { + Mnemonic string `json:"mnemonic"` +} + +func NewInput() *input { + res := new(input) + res.BasicInput = new(internal.BasicInput) + return res +} + +func NewResults(networkMode string) internal.PerfResult { + return internal.PerfResult{ + AccountCreate: {MethodName: AccountCreate, NetworkMode: networkMode}, + } +} + +func main() { + prep := NewInput() + err := internal.Prepare(prep, nil) + if err != nil { + fmt.Println("Error preparing the environment:", err) + os.Exit(1) + } + + res := NewResults(prep.NetworkMode) + for i := 0; i < prep.Times; i++ { + err = iterate(prep, res) + if err != nil { + fmt.Println("Error making iteration:", err) + os.Exit(1) + } + } + err = internal.After(res) + if err != nil { + fmt.Println("Error after the test:", err) + os.Exit(1) + } +} + +func iterate(prep *input, result internal.PerfResult) error { + workspace, err := os.MkdirTemp("", "workspace") + prep.Workspace = workspace + if err != nil { + return err + } + defer os.RemoveAll(workspace) + fmt.Println("Created temporary directory:", workspace) + + var currentOperation atomic.String + done := make(chan struct{}) + wait := make(chan map[string][]byte) + + err = internal.StartWithTracing(¤tOperation, done, wait) + if err != nil { + return err + } + + err = internal.ExecuteCommand(internal.GrpcMetricsSetParameters()) + if err != nil { + return err + } + + walletStr, err := exec.Command("bash", "-c", internal.GrpcWalletCreate(workspace)).Output() + if err != nil { + return err + } + + var wallet wallet + err = json.Unmarshal(walletStr, &wallet) + if err != nil { + return err + } + + grpcurlCommands := []internal.Command{ + {internal.GrpcWalletCreateSession(wallet.Mnemonic), ""}, + accountCreate(prep), + } + + err = internal.CollectMeasurements(grpcurlCommands, ¤tOperation, result, done, wait) + if err != nil { + return err + } + return nil +} + +func accountCreate(prep *input) internal.Command { + if prep.NetworkMode != internal.NetworkLocal { + return internal.Command{Command: internal.GrpcAccountCreate(prep.Workspace, "2", prep.NodesConfig), Name: AccountCreate} + } + return internal.Command{Command: internal.GrpcAccountCreate(prep.Workspace, "1", ""), Name: AccountCreate} +} diff --git a/cmd/perfstand/account_select/main.go b/cmd/perfstand/account_select/main.go new file mode 100644 index 000000000..092dcd6c5 --- /dev/null +++ b/cmd/perfstand/account_select/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "go.uber.org/atomic" + + "github.com/anyproto/anytype-heart/cmd/perfstand/internal" +) + +const AccountSelect = "AccountSelect" +const WorkspaceOpen = "WorkspaceOpen" +const WorkspaceCreate = "WorkspaceCreate" + +type input struct { + *internal.BasicInput + RootPath string `json:"root_path"` + AccHash string `json:"acc_hash"` + Mnemonic string `json:"mnemonic"` + Space string `json:"space"` +} + +func NewInput() *input { + res := new(input) + res.BasicInput = new(internal.BasicInput) + return res +} + +func NewResults(networkMode string) internal.PerfResult { + return internal.PerfResult{ + AccountSelect: {MethodName: AccountSelect, NetworkMode: networkMode}, + WorkspaceOpen: {MethodName: WorkspaceOpen, NetworkMode: networkMode}, + WorkspaceCreate: {MethodName: WorkspaceCreate, NetworkMode: networkMode}, + } +} + +func main() { + prep := NewInput() + err := internal.Prepare(prep, extractAcc) + if err != nil { + fmt.Println("Error preparing the environment:", err) + os.Exit(1) + } + defer os.RemoveAll(prep.Workspace) + + res := NewResults(prep.NetworkMode) + for i := 0; i < prep.Times; i++ { + err = iterate(prep, res) + if err != nil { + fmt.Println("Error making iteration:", err) + os.Exit(1) + } + } + err = internal.After(res) + if err != nil { + fmt.Println("Error after the test:", err) + os.Exit(1) + } +} + +func extractAcc(input *input) error { + err := internal.UnpackZip(filepath.Join(input.RootPath, input.AccHash+".zip"), input.Workspace) + if err != nil { + return err + } + fmt.Println("Unpacked files to:", input.Workspace) + return nil +} + +func iterate(prep *input, result internal.PerfResult) error { + var currentOperation atomic.String + done := make(chan struct{}) + wait := make(chan map[string][]byte) + + err := internal.StartWithTracing(¤tOperation, done, wait) + if err != nil { + return err + } + + grpcurlCommands := []internal.Command{ + {internal.GrpcMetricsSetParameters(), ""}, + {internal.GrpcWalletRecover(prep.Workspace, prep.Mnemonic), ""}, + {internal.GrpcWalletCreateSession(prep.Mnemonic), ""}, + accountSelect(prep), + {internal.GrpcWorkspaceOpen(prep.Space), WorkspaceOpen}, + {internal.GrpcWorkspaceCreate(), WorkspaceCreate}, + } + + err = internal.CollectMeasurements(grpcurlCommands, ¤tOperation, result, done, wait) + if err != nil { + return err + } + return nil +} + +func accountSelect(prep *input) internal.Command { + if prep.NetworkMode != internal.NetworkLocal { + return internal.Command{Command: internal.GrpcAccountSelect(prep.AccHash, prep.Workspace, "2", prep.NodesConfig), Name: AccountSelect} + } + return internal.Command{Command: internal.GrpcAccountSelect(prep.AccHash, prep.Workspace, "1", ""), Name: AccountSelect} +} diff --git a/cmd/perfstand/internal/util.go b/cmd/perfstand/internal/util.go new file mode 100644 index 000000000..424fb92e9 --- /dev/null +++ b/cmd/perfstand/internal/util.go @@ -0,0 +1,512 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "go.uber.org/atomic" +) + +const NetworkLocal = "local" +const NetworkStaging = "staging" + +type Event struct { + MethodName string `json:"method_name"` + Duration int64 `json:"duration"` + MiddlewareVersion string `json:"middleware_version"` + Network string `json:"network"` +} + +func GetMiddlewareVersion() (string, error) { + out, err := exec.Command("git", "describe", "--tags", "--always").Output() + if err != nil { + return "", err + } + middlewareVersion := strings.Trim(string(out), "\n") + return middlewareVersion, nil +} + +func SendResultsToHttp(apiKey string, events []Event) error { + payload := map[string]interface{}{ + "api_key": apiKey, + "events": events, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + req, err := http.NewRequest("POST", "https://telemetry.anytype.io/perfstand", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + fmt.Println("Results sent successfully!") + return nil +} + +func KillServer() error { + return ExecuteCommand("kill $(lsof -i :31007 -t) ; echo \"Server killed\"") +} + +func ExecuteCommand(command string) error { + fmt.Println(command) + cmd := exec.Command("bash", "-c", command) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return err + } + return nil +} + +func UnpackZip(path string, workspace string) error { + return ExecuteCommand("unzip -o " + path + " -d " + workspace) +} + +func BuildAnytype(err error) error { + buildServer := exec.Command("make", "build-server") + buildServer.Stdout = os.Stdout + buildServer.Stderr = os.Stderr + buildServer.Env = append(os.Environ(), "TAGS=noauth") + + err = buildServer.Run() + return err +} + +func LoadEnv(env string) (string, error) { + res := os.Getenv(env) + if res == "" { + return "", fmt.Errorf("environment variable %s is not set", env) + } + return res, nil +} + +func SetupWd() (string, error) { + err := os.Chdir("../../..") + if err != nil { + return "", err + } + + getwd, err := os.Getwd() + if err != nil { + return "", err + } + + fmt.Println("Current working directory:", getwd) + return getwd, nil +} + +func GrpcWorkspaceOpen(workspace string) string { + return `grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{ + "spaceId": "` + workspace + `" + }' localhost:31007 anytype.ClientCommands.WorkspaceOpen` +} + +func GrpcWorkspaceCreate() string { + return `grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{ + }' localhost:31007 anytype.ClientCommands.WorkspaceCreate` +} + +func GrpcAccountSelect(accHash, workspace, networkMode, staging string) string { + return `grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{ + "id": "` + accHash + `", + "rootPath": "` + workspace + `", + "disableLocalNetworkSync": false, + "networkMode": ` + networkMode + `, + "networkCustomConfigFilePath": "` + staging + `" + }' localhost:31007 anytype.ClientCommands.AccountSelect` +} + +func GrpcWalletCreateSession(mnemonic string) string { + return `grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{ + "mnemonic": "` + mnemonic + `" + }' localhost:31007 anytype.ClientCommands.WalletCreateSession` +} + +func GrpcWalletRecover(workspace, mnemonic string) string { + return `grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{ + "rootPath": "` + workspace + `", + "mnemonic": "` + mnemonic + `" + }' localhost:31007 anytype.ClientCommands.WalletRecover` +} + +func GrpcWalletCreate(workspace string) string { + return `grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{ + "rootPath": "` + workspace + `" + }' localhost:31007 anytype.ClientCommands.WalletCreate` +} + +func GrpcAccountCreate(workspace, networkMode, staging string) string { + return `grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{ + "icon": 13, + "networkMode": ` + networkMode + `, + "storePath": "` + workspace + `", + "networkCustomConfigFilePath": "` + staging + `" + }' localhost:31007 anytype.ClientCommands.AccountCreate` +} + +func GrpcMetricsSetParameters() string { + return `grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{ + "platform": "test", + "version": "0.0.0-test" + }' localhost:31007 anytype.ClientCommands.MetricsSetParameters` +} + +func StartAnytypeBackground() error { + runServer := exec.Command("./dist/server") + runServer.Stdout = os.Stdout + runServer.Stderr = os.Stderr + runServer.Env = append(os.Environ(), `ANYPROF=:6060`) + err := runServer.Start() + if err != nil { + return err + } + + // Wait for the server to start + for { + err = ExecuteCommand(`pids=$(lsof -i :31007 -t) && [ -n "$pids" ] && echo "Found process: $pids" || { echo "No process found"; exit 1; }`) + if err == nil { + break + } else { + time.Sleep(10 * time.Second) + fmt.Println("Waiting for the server to start...", err) + } + } + return nil +} + +func CollectGoroutines() ([]byte, error) { + url := "http://localhost:6060/debug/pprof/goroutine?debug=1" + + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} + +type Command struct { + Command string + Name string +} + +type MethodResult struct { + MethodName string + NetworkMode string + Measurements []int64 + CurrentMax int64 + CurrentMaxIndex int64 + MaxTrace []byte +} + +func (mr *MethodResult) TryUpdateTrace(trace []byte) { + mrLen := len(mr.Measurements) - 1 + if mr.CurrentMax < mr.Measurements[mrLen] { + mr.CurrentMax = mr.Measurements[mrLen] + mr.MaxTrace = trace + } +} + +func Convert(res map[string]*MethodResult) ([]Event, error) { + middlewareVersion, err := GetMiddlewareVersion() + if err != nil { + return nil, err + } + + var events []Event + for _, value := range res { + for _, duration := range value.Measurements { + events = append(events, Event{ + MethodName: value.MethodName, + Duration: duration, + MiddlewareVersion: middlewareVersion, + Network: value.NetworkMode, + }) + } + } + return events, nil +} + +type PerfResult = map[string]*MethodResult + +func SaveMaxTracesToFiles(perfResult PerfResult) error { + for key, result := range perfResult { + if result.CurrentMax > 0 { + fileName := fmt.Sprintf("goroutine_%s_%d_%d.log", result.MethodName, result.CurrentMax, result.CurrentMaxIndex) + err := os.WriteFile(fileName, result.MaxTrace, 0644) + if err != nil { + return err + } + fmt.Printf("Saved MaxTrace for method %s to file: %s\n", key, fileName) + } + } + return nil +} + +func AssertFileExists(filePath string) error { + _, err := os.Stat(filePath) + if err != nil { + return err + } + return nil +} + +func TraceServer(currentOperation *atomic.String, done chan struct{}, wait chan map[string][]byte) { + currentTraces := make(map[string][][]byte) + for { + select { + case <-done: + traces := make(map[string][]byte) + for key, value := range currentTraces { + if len(value) > 0 { + traces[key] = value[len(value)/2] + } else { + traces[key] = nil + } + } + wait <- traces + fmt.Println("Goroutine stopped") + default: + time.Sleep(1 * time.Second) + currentOperation := currentOperation.Load() + if currentOperation != "" { + bytes, err := CollectGoroutines() + if err != nil { + fmt.Println("Error collecting goroutines:", err) + } else { + if trace, ok := currentTraces[currentOperation]; ok { + currentTraces[currentOperation] = append(trace, bytes) + } else { + currentTraces[currentOperation] = [][]byte{bytes} + } + } + } + } + } +} + +func Measure(grpcurlCommands []Command, currentOperation *atomic.String, result PerfResult) error { + for _, cmd := range grpcurlCommands { + if cmd.Name != "" { + currentOperation.Store(cmd.Name) + } + start := time.Now().UnixMilli() + err := ExecuteCommand(cmd.Command) + if err != nil { + return err + } + if val, ok := result[cmd.Name]; ok { + val.Measurements = append(val.Measurements, time.Now().UnixMilli()-start) + } + currentOperation.Store("") + } + return nil +} + +func StartWithTracing(currentOperation *atomic.String, done chan struct{}, wait chan map[string][]byte) error { + go TraceServer(currentOperation, done, wait) + err := KillServer() + if err != nil { + return err + } + + err = StartAnytypeBackground() + if err != nil { + return err + } + return nil +} + +func CollectMeasurements( + grpcurlCommands []Command, + currentOperation *atomic.String, + result PerfResult, + done chan struct{}, + wait chan map[string][]byte, +) error { + err := Measure(grpcurlCommands, currentOperation, result) + if err != nil { + return err + } + + err = KillServer() + if err != nil { + return err + } + + fmt.Println("All commands executed successfully.") + close(done) + traces := <-wait + for key, value := range traces { + result[key].TryUpdateTrace(value) + } + return nil +} + +func ReadJson[T any](t *T, path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + err = json.Unmarshal(data, &t) + if err != nil { + return err + } + return nil +} + +type BasicInput struct { + NetworkMode string `json:"network_mode"` + NodesConfig string `json:"nodes_config"` + Times int `json:"times,omitempty"` + Workspace string `json:"workspace,omitempty"` +} + +type BasicInputtable interface { + ValidateNetwork() error + SetTimes(times int) + SetWorkspace(workspace string) +} + +func (bi *BasicInput) ValidateNetwork() error { + if bi.NetworkMode != NetworkLocal && bi.NetworkMode != NetworkStaging { + return fmt.Errorf("network mode should be either 'local' or 'staging', got: %s", bi.NetworkMode) + } + if bi.NetworkMode == NetworkStaging { + wd, err := os.Getwd() + if err != nil { + return err + } + bi.NodesConfig = filepath.Join(wd, bi.NodesConfig) + err = AssertFileExists(bi.NodesConfig) + if err != nil { + return err + } + } + return nil +} + +func (bi *BasicInput) SetTimes(times int) { + bi.Times = times +} + +func (bi *BasicInput) SetWorkspace(workspace string) { + bi.Workspace = workspace +} + +func Prepare[T BasicInputtable](prep T, f func(T) error) error { + configPath := os.Args[1] + err := AssertFileExists(configPath) + if err != nil { + return err + } + + times, err := strconv.Atoi(os.Args[2]) + if err != nil { + return err + } + if times <= 0 { + return fmt.Errorf("times should be greater than 0, got: %d", times) + } + prep.SetTimes(times) + + err = ReadJson(&prep, configPath) + if err != nil { + return err + } + + workspace, err := os.MkdirTemp("", "workspace") + if err != nil { + return err + } + fmt.Println("Created temporary directory:", workspace) + prep.SetWorkspace(workspace) + + _, err = SetupWd() + if err != nil { + return err + } + + err = prep.ValidateNetwork() + if err != nil { + return err + } + + if f != nil { + err = f(prep) + if err != nil { + return err + } + } + + err = BuildAnytype(err) + if err != nil { + return err + } + return nil +} + +func SendResults(res PerfResult) error { + apiKey, err := LoadEnv("CH_API_KEY") + if err != nil { + return err + } + + events, err := Convert(res) + if err != nil { + return err + } + + err = SendResultsToHttp(apiKey, events) + if err != nil { + return err + } + + for key, value := range res { + fmt.Printf("### Results::%s: %v\n", key, value.Measurements) + } + return nil +} + +func After(res PerfResult) error { + err := SendResults(res) + if err != nil { + return err + } + + err = SaveMaxTracesToFiles(res) + if err != nil { + return err + } + return nil +}