Skip to content

Commit

Permalink
feat: add stats collector for pretty printing (#74)
Browse files Browse the repository at this point in the history
Signed-off-by: Gaius <[email protected]>
  • Loading branch information
gaius-qi authored Nov 1, 2024
1 parent cac20b3 commit 7312e19
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 17 deletions.
23 changes: 10 additions & 13 deletions benchmark/cmd/dfbench/dragonfly.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ package dfbench
import (
"context"
"fmt"
"strings"

"github.com/dragonflyoss/perf-tests/benchmark/pkg/backend"
"github.com/dragonflyoss/perf-tests/benchmark/pkg/config"
"github.com/dragonflyoss/perf-tests/benchmark/pkg/dragonfly"
"github.com/dragonflyoss/perf-tests/benchmark/pkg/stats"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand All @@ -40,7 +42,7 @@ var dragonflyCmd = &cobra.Command{
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()

logrus.Infof("running dragonfly benchmark %d times", cfg.Dragonfly.Number)
logrus.Debugf("running dragonfly benchmark %d times", cfg.Dragonfly.Number)
return runDragonfly(ctx, cfg)
},
}
Expand All @@ -51,7 +53,7 @@ func init() {
flags.Uint32VarP(&cfg.Dragonfly.Number, "number", "n", cfg.Dragonfly.Number, "Specify the number of times to run the dragonfly benchmark")
flags.StringVarP(&cfg.Dragonfly.Namespace, "namespace", "s", cfg.Dragonfly.Namespace, "Specify the namespace to use for the dragonfly benchmark")
flags.StringVarP(&cfg.Dragonfly.Downloader, "downloader", "d", cfg.Dragonfly.Downloader, "Specify the downloader to use for the dragonfly benchmark [dfget, proxy], default is dfget")
flags.StringVar(&cfg.Dragonfly.FileSizeLevel, "file-size-level", cfg.Dragonfly.FileSizeLevel, "Specify the file size level to use for the dragonfly benchmark [nano, micro, small, medium, large, huge], default is running all levels")
flags.StringVar(&cfg.Dragonfly.FileSizeLevel, "file-size-level", cfg.Dragonfly.FileSizeLevel, "Specify the file size level to use for the dragonfly benchmark [nano, micro, small, medium, large, xlarge, xxlarge], default is running all levels")

if err := viper.BindPFlags(flags); err != nil {
panic(fmt.Errorf("bind cache dragonfly flags to viper: %w", err))
Expand All @@ -60,42 +62,37 @@ func init() {

// runDragonfly runs the dragonfly benchmark.
func runDragonfly(ctx context.Context, cfg *config.Config) error {
stats := stats.New()
fileServer := backend.NewFileServer(cfg.Dragonfly.Namespace)
dragonfly := dragonfly.New(cfg.Dragonfly.Namespace, fileServer)
dragonfly := dragonfly.New(cfg.Dragonfly.Namespace, fileServer, stats)

// If file size level is not specified, run all file size levels.
if cfg.Dragonfly.FileSizeLevel == "" {
logrus.Infof("running dragonfly benchmark for all file size levels by downloader %s", cfg.Dragonfly.Downloader)
fmt.Printf("Running benchmark for all size levels by %s ...\n", strings.ToUpper(cfg.Dragonfly.Downloader))
if err := dragonfly.Run(ctx, cfg.Dragonfly.Downloader); err != nil {
logrus.Errorf("failed to run dragonfly benchmark: %v", err)
return err
}
logrus.Info("completed dragonfly benchmark")
stats.PrettyPrint()

logrus.Info("cleaning up dragonfly benchmark")
if err := dragonfly.Cleanup(ctx); err != nil {
logrus.Errorf("failed to cleanup dragonfly benchmark: %v", err)
return err
}

logrus.Info("completed dragonfly cleanup")
return nil
}

// Run the benchmark for the specified file size level.
logrus.Infof("running dragonfly benchmark for file size level %s by downloader %s", cfg.Dragonfly.FileSizeLevel, cfg.Dragonfly.Downloader)
fmt.Printf("Running benchmark for %s size level by %s ...\n", strings.ToUpper(cfg.Dragonfly.FileSizeLevel), strings.ToUpper(cfg.Dragonfly.Downloader))
if err := dragonfly.RunByFileSizes(ctx, cfg.Dragonfly.Downloader, backend.FileSizeLevel(cfg.Dragonfly.FileSizeLevel)); err != nil {
logrus.Errorf("failed to run dragonfly benchmark: %v", err)
return err
}
logrus.Info("completed dragonfly benchmark")
stats.PrettyPrint()

logrus.Info("cleaning up dragonfly benchmark")
if err := dragonfly.Cleanup(ctx); err != nil {
logrus.Errorf("failed to cleanup dragonfly benchmark: %v", err)
return err
}

logrus.Info("completed dragonfly cleanup")
return nil
}
2 changes: 2 additions & 0 deletions benchmark/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22.4

require (
github.com/google/uuid v1.4.0
github.com/olekukonko/tablewriter v0.0.5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
Expand All @@ -15,6 +16,7 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions benchmark/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
19 changes: 15 additions & 4 deletions benchmark/pkg/dragonfly/dragonfly.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import (
"errors"
"fmt"
"path"
"time"

"github.com/dragonflyoss/perf-tests/benchmark/pkg/backend"
"github.com/dragonflyoss/perf-tests/benchmark/pkg/config"
"github.com/dragonflyoss/perf-tests/benchmark/pkg/stats"
"github.com/dragonflyoss/perf-tests/benchmark/pkg/util"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -54,13 +56,19 @@ type Dragonfly interface {

// dragonfly implements the Dragonfly interface.
type dragonfly struct {
namespace string
// namespace is the namespace of the benchmark.
namespace string

// fileServer is the file server of the benchmark.
fileServer backend.FileServer

// stats is the statistics of the benchmark.
stats stats.Stats
}

// New creates a new benchmark runner for Dragonfly.
func New(namespace string, fileServer backend.FileServer) Dragonfly {
return &dragonfly{namespace, fileServer}
func New(namespace string, fileServer backend.FileServer, stats stats.Stats) Dragonfly {
return &dragonfly{namespace, fileServer, stats}
}

// Run runs all benchmarks by downloader.
Expand Down Expand Up @@ -209,12 +217,14 @@ func (d *dragonfly) downloadFileByDfget(ctx context.Context, podExec *util.PodEx
return err
}

createdAt := time.Now()
output, err := podExec.Command(ctx, "sh", "-c", fmt.Sprintf("dfget '%s' --output %s", downloadURL.String(), outputPath)).CombinedOutput()
if err != nil {
logrus.Errorf("failed to download file: %v \nmessage: %s", err, string(output))
return err
}

d.stats.AddDownload(downloadURL, config.DownloaderDfget, fileSizeLevel, createdAt, time.Now())
logrus.Debugf("dfget output: %s", string(output))
return nil
}
Expand Down Expand Up @@ -261,11 +271,13 @@ func (d *dragonfly) downloadFileByProxy(ctx context.Context, podExec *util.PodEx
return err
}

createdAt := time.Now()
output, err := podExec.Command(ctx, "sh", "-c", fmt.Sprintf("curl -x %s '%s' --output %s", "http://127.0.0.1:4001", downloadURL.String(), outputPath)).CombinedOutput()
if err != nil {
logrus.Errorf("failed to download file: %v \nmessage: %s", err, string(output))
return err
}
d.stats.AddDownload(downloadURL, config.DownloaderProxy, fileSizeLevel, createdAt, time.Now())

logrus.Debugf("curl output: %s", string(output))
return nil
Expand Down Expand Up @@ -314,7 +326,6 @@ func (d *dragonfly) getClientPods(ctx context.Context) ([]string, error) {
logrus.Errorf("no client pod found")
return nil, errors.New("no client pod found")
}

return pods, nil
}

Expand Down
166 changes: 166 additions & 0 deletions benchmark/pkg/stats/stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright 2024 The Dragonfly 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 stats

import (
"fmt"
"net/url"
"os"
"sync"
"time"

"github.com/dragonflyoss/perf-tests/benchmark/pkg/backend"
"github.com/dragonflyoss/perf-tests/benchmark/pkg/config"
"github.com/google/uuid"
"github.com/olekukonko/tablewriter"
)

// Stats represents the statistics of the benchmark.
type Stats interface {
// AddDownload adds a download record to the statistics.
AddDownload(*url.URL, string, backend.FileSizeLevel, time.Time, time.Time)

// GetDownloads returns all download records.
GetDownloads() []*Download

// PrettyPrint prints the statistics in a pretty format.
PrettyPrint()
}

// stats implements the Stats interface.
type stats struct {
// downloads stores the download statistics.
downloads *sync.Map
}

// Download represents the download statistics.
type Download struct {
// url is the URL of the file.
url *url.URL

// downloader is the downloader used to download the file.
downloader string

// fileSizeLevel is the file size level of the file.
fileSizeLevel backend.FileSizeLevel

// cost is the time cost of downloading the file.
cost time.Duration

// createdAt is the time when the download started.
createdAt time.Time

// finishedAt is the time when the download finished.
finishedAt time.Time
}

// New creates a new Stats instance.
func New() Stats {
return &stats{downloads: &sync.Map{}}
}

// AddDownload adds a download record to the statistics.
func (s *stats) AddDownload(url *url.URL, downloader string, fileSizeLevel backend.FileSizeLevel, createdAt time.Time, finishedAt time.Time) {
s.downloads.Store(uuid.New().String(), &Download{
url: url,
downloader: downloader,
fileSizeLevel: fileSizeLevel,
cost: finishedAt.Sub(createdAt),
createdAt: createdAt,
finishedAt: finishedAt,
})
}

func (s *stats) GetDownloads() []*Download {
downloads := make([]*Download, 0)
s.downloads.Range(func(key, value interface{}) bool {
downloads = append(downloads, value.(*Download))
return true
})

return downloads
}

// PrettyPrint prints the statistics in a pretty format.
func (s *stats) PrettyPrint() {
downloads := s.GetDownloads()

proxyDownloads := make(map[backend.FileSizeLevel][]*Download)
dfgetDownloads := make(map[backend.FileSizeLevel][]*Download)

for _, download := range downloads {
switch download.downloader {
case config.DownloaderDfget:
dfgetDownloads[download.fileSizeLevel] = append(dfgetDownloads[download.fileSizeLevel], download)
case config.DownloaderProxy:
proxyDownloads[download.fileSizeLevel] = append(proxyDownloads[download.fileSizeLevel], download)
}
}

if len(dfgetDownloads) != 0 {
printTable(dfgetDownloads)
}

if len(proxyDownloads) != 0 {
printTable(proxyDownloads)
}
}

// printTable prints the download statistics in a table format.
func printTable(downloads map[backend.FileSizeLevel][]*Download) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"File Size Level", "Times", "Min Cost", "Max Cost", "Avg Cost"})

for fileSizeLevel, records := range downloads {
var minCost, maxCost, totalCost time.Duration
if len(records) > 0 {
minCost = records[0].cost
maxCost = records[0].cost
}

for _, record := range records {
if record.cost < minCost {
minCost = record.cost
}

if record.cost > maxCost {
maxCost = record.cost
}

totalCost += record.cost
}

avgCost := totalCost / time.Duration(len(records))
table.Append([]string{
fmt.Sprintf("%s", fileSizeLevel),
fmt.Sprintf("%d", len(records)),
formatDuration(minCost),
formatDuration(maxCost),
formatDuration(avgCost),
})
}

table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetRowLine(true)
table.Render()
}

// formatDuration formats the duration to a string.
func formatDuration(d time.Duration) string {
ms := float64(d) / float64(time.Millisecond)
return fmt.Sprintf("%.2fms", ms)
}

0 comments on commit 7312e19

Please sign in to comment.