github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/bench/tools/aisloader/print.go (about)

     1  // Package aisloader
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  
     6  package aisloader
     7  
     8  import (
     9  	"encoding/json"
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"math"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/NVIDIA/aistore/bench/tools/aisloader/stats"
    19  	"github.com/NVIDIA/aistore/cmn"
    20  	"github.com/NVIDIA/aistore/cmn/cos"
    21  	"github.com/NVIDIA/aistore/cmn/debug"
    22  	jsoniter "github.com/json-iterator/go"
    23  )
    24  
    25  var examples = `# 1. Cleanup (i.e., destroy) an existing bucket:
    26       $ aisloader -bucket=ais://abc -duration 0s -totalputsize=0 -cleanup=true
    27       $ aisloader -bucket=mybucket -provider=aws -cleanup=true -duration 0s -totalputsize=0
    28  # 2. Timed 100% PUT via 8 parallel workers into an ais bucket (that may or may not exists) with
    29       complete cleanup upon termination (NOTE: cleanup involves emptying the specified bucket):
    30       $ aisloader -bucket=ais://abc -duration 30s -numworkers=8 -minsize=1K -maxsize=1K -pctput=100 --cleanup=true
    31  # 3. Timed (for 1h) 100% GET from an existing AWS S3 bucket, no cleanup:
    32       $ aisloader -bucket=nvaws -duration 1h -numworkers=30 -pctput=0 -provider=aws -cleanup=false
    33  # or, same:
    34       $ aisloader -bucket=s3://nvaws -duration 1h -numworkers=30 -pctput=0 -cleanup=false
    35  
    36  # 4. Mixed 30%/70% PUT and GET of variable-size objects to/from an AWS S3 bucket.
    37  #    PUT will generate random object names and the duration is limited only by the total size (10GB).
    38  #    Cleanup enabled - upon completion all generated objects and the bucket itself will be deleted:
    39       $ aisloader -bucket=s3://nvaws -duration 0s -cleanup=true -numworkers=3 -minsize=1024 -maxsize=1MB -pctput=30 -totalputsize=10G
    40  # 5. PUT 1GB total into an ais bucket with cleanup disabled, object size = 1MB, duration unlimited:
    41       $ aisloader -bucket=ais://abc -cleanup=false -totalputsize=1G -duration=0 -minsize=1MB -maxsize=1MB -numworkers=8 -pctput=100
    42  # 6. 100% GET from an ais bucket (no cleanup):
    43       $ aisloader -bucket=ais://abc -duration 15s -numworkers=3 -pctput=0 -cleanup=false
    44  # or, same:
    45       $ aisloader -bucket=abc -provider=ais -duration 5s -numworkers=3 -pctput=0 -cleanup=false
    46  
    47  # 7. PUT 2000 objects named as 'aisloader/hex({0..2000}{loaderid})', cleanup upon exit:
    48       $ aisloader -bucket=ais://abc -duration 10s -numworkers=3 -loaderid=11 -loadernum=20 -maxputs=2000 -objNamePrefix="aisloader" -cleanup=true
    49  # 8. Use random object names and loaderID to report statistics:
    50       $ aisloader -loaderid=10
    51  # 9. PUT objects with random name generation being based on the specified loaderID and the total number of concurrent aisloaders:
    52       $ aisloader -loaderid=10 -loadernum=20
    53  # 10. Same as above except that loaderID is computed by the aisloader as hash(loaderstring) & 0xff:
    54       $ aisloader -loaderid=loaderstring -loaderidhashlen=8
    55  # 11. Print loaderID and exit (all 3 examples below) with the resulting loaderID commented on the right:",
    56       $ aisloader -getloaderid 			# 0x0
    57       $ aisloader -loaderid=10 -getloaderid	# 0xa
    58       $ aisloader -loaderid=loaderstring -loaderidhashlen=8 -getloaderid	# 0xdb
    59  # 12. Timed 100% GET _directly_ from S3 bucket (notice '-s3endpoint' command line):
    60       $ aisloader -bucket=s3://xyz -cleanup=false -numworkers=8 -pctput=0 -duration=10m -s3endpoint=https://s3.amazonaws.com
    61  # 13. PUT approx. 8000 files into s3 bucket directly, skip printing usage and defaults (NOTE: aistore is not being used):
    62       $ aisloader -bucket=s3://xyz -cleanup=false -minsize=16B -maxsize=16B -numworkers=8 -pctput=100 -totalputsize=128k -s3endpoint=https://s3.amazonaws.com -quiet
    63  `
    64  
    65  const readme = cmn.GitHubHome + "/blob/main/docs/howto_benchmark.md"
    66  
    67  func printUsage(f *flag.FlagSet) {
    68  	fmt.Printf("aisloader v%s (build %s)\n", _version, _buildtime)
    69  	fmt.Println("\nAbout")
    70  	fmt.Println("=====")
    71  	fmt.Println("AIS Loader (aisloader) is a benchmarking tool to measure AIStore performance.")
    72  	fmt.Println("It's a load generator that has been developed to benchmark and stress-test AIStore")
    73  	fmt.Println("but can be easily extended to benchmark any S3-compatible backend.")
    74  	fmt.Println("For usage, run: `aisloader`, or `aisloader usage`, or `aisloader --help`.")
    75  	fmt.Println("Further details at " + readme)
    76  
    77  	fmt.Println("\nCommand-line options")
    78  	fmt.Println("====================")
    79  	f.PrintDefaults()
    80  	fmt.Println()
    81  
    82  	fmt.Println("Examples")
    83  	fmt.Println("========")
    84  	fmt.Print(examples)
    85  }
    86  
    87  // prettyNumber converts a number to format like 1,234,567
    88  func prettyNumber(n int64) string {
    89  	if n < 1000 {
    90  		return strconv.FormatInt(n, 10)
    91  	}
    92  	return fmt.Sprintf("%s,%03d", prettyNumber(n/1000), n%1000)
    93  }
    94  
    95  // prettyBytes converts number of bytes to something like 4.7G, 2.8K, etc
    96  func prettyBytes(n int64) string {
    97  	if n <= 0 { // process special case that B2S do not cover
    98  		return "-"
    99  	}
   100  	return cos.ToSizeIEC(n, 1)
   101  }
   102  
   103  func prettySpeed(n int64) string {
   104  	if n <= 0 {
   105  		return "-"
   106  	}
   107  	return cos.ToSizeIEC(n, 2) + "/s"
   108  }
   109  
   110  // prettyDuration converts an integer representing a time in nano second to a string
   111  func prettyDuration(t int64) string {
   112  	d := time.Duration(t).String()
   113  	i := strings.Index(d, ".")
   114  	if i < 0 {
   115  		return d
   116  	}
   117  	out := make([]byte, i+1, 32)
   118  	copy(out, d[0:i+1])
   119  	for j := i + 1; j < len(d); j++ {
   120  		if d[j] > '9' || d[j] < '0' {
   121  			out = append(out, d[j])
   122  		} else if j < i+4 {
   123  			out = append(out, d[j])
   124  		}
   125  	}
   126  	return string(out)
   127  }
   128  
   129  // prettyLatency combines three latency min, avg and max into a string
   130  func prettyLatency(minL, avgL, maxL int64) string {
   131  	return fmt.Sprintf("%-11s%-11s%-11s", prettyDuration(minL), prettyDuration(avgL), prettyDuration(maxL))
   132  }
   133  
   134  func now() string {
   135  	return time.Now().Format(cos.StampSec)
   136  }
   137  
   138  func preWriteStats(to io.Writer, jsonFormat bool) {
   139  	if !jsonFormat {
   140  		fmt.Fprintln(to)
   141  		fmt.Fprintf(to, statsPrintHeader,
   142  			"Time", "OP", "Count", "Size (Total)", "Latency (min, avg, max)", "Throughput (Avg)", "Errors (Total)")
   143  	} else {
   144  		fmt.Fprint(to, "[")
   145  	}
   146  }
   147  
   148  func postWriteStats(to io.Writer, jsonFormat bool) {
   149  	if jsonFormat {
   150  		fmt.Fprintln(to)
   151  		fmt.Fprintln(to, "]")
   152  	}
   153  }
   154  
   155  func finalizeStats(to io.Writer) {
   156  	accumulatedStats.aggregate(&intervalStats)
   157  	writeStats(to, runParams.jsonFormat, true /* final */, &intervalStats, &accumulatedStats)
   158  	postWriteStats(to, runParams.jsonFormat)
   159  
   160  	// reset gauges, otherwise they would stay at last send value
   161  	stats.ResetMetricsGauges(statsdC)
   162  }
   163  
   164  func writeFinalStats(to io.Writer, jsonFormat bool, s *sts) {
   165  	if !jsonFormat {
   166  		writeHumanReadibleFinalStats(to, s)
   167  	} else {
   168  		writeStatsJSON(to, s, false)
   169  	}
   170  }
   171  
   172  func writeIntervalStats(to io.Writer, jsonFormat bool, s, t *sts) {
   173  	if !jsonFormat {
   174  		writeHumanReadibleIntervalStats(to, s, t)
   175  	} else {
   176  		writeStatsJSON(to, s)
   177  	}
   178  }
   179  
   180  func jsonStatsFromReq(r stats.HTTPReq) *jsonStats {
   181  	jStats := &jsonStats{
   182  		Cnt:        r.Total(),
   183  		Bytes:      r.TotalBytes(),
   184  		Start:      r.Start(),
   185  		Duration:   time.Since(r.Start()),
   186  		Errs:       r.TotalErrs(),
   187  		Latency:    r.AvgLatency(),
   188  		MinLatency: r.MinLatency(),
   189  		MaxLatency: r.MaxLatency(),
   190  		Throughput: r.Throughput(r.Start(), time.Now()),
   191  	}
   192  
   193  	return jStats
   194  }
   195  
   196  func writeStatsJSON(to io.Writer, s *sts, withcomma ...bool) {
   197  	jStats := struct {
   198  		Get *jsonStats `json:"get"`
   199  		Put *jsonStats `json:"put"`
   200  		Cfg *jsonStats `json:"cfg"`
   201  	}{
   202  		Get: jsonStatsFromReq(s.get),
   203  		Put: jsonStatsFromReq(s.put),
   204  		Cfg: jsonStatsFromReq(s.getConfig),
   205  	}
   206  
   207  	jsonOutput, err := json.MarshalIndent(jStats, "", "  ")
   208  	cos.AssertNoErr(err)
   209  	fmt.Fprintf(to, "\n%s", string(jsonOutput))
   210  	// print comma by default
   211  	if len(withcomma) == 0 || withcomma[0] {
   212  		fmt.Fprint(to, ",")
   213  	}
   214  }
   215  
   216  func fprintf(w io.Writer, format string, a ...any) {
   217  	_, err := fmt.Fprintf(w, format, a...)
   218  	debug.AssertNoErr(err)
   219  }
   220  
   221  func writeHumanReadibleIntervalStats(to io.Writer, s, t *sts) {
   222  	p := fprintf
   223  	pn := prettyNumber
   224  	pb := prettyBytes
   225  	ps := prettySpeed
   226  	pl := prettyLatency
   227  	pt := now
   228  
   229  	workOrderResLen := int64(len(resCh))
   230  	// show interval stats; some fields are shown of both interval and total, for example, gets, puts, etc
   231  	errs := "-"
   232  	if t.put.TotalErrs() != 0 {
   233  		errs = pn(s.put.TotalErrs()) + " (" + pn(t.put.TotalErrs()) + ")"
   234  	}
   235  	if s.put.Total() != 0 {
   236  		p(to, statsPrintHeader, pt(), "PUT",
   237  			pn(s.put.Total())+" ("+pn(t.put.Total())+" "+pn(putPending)+" "+pn(workOrderResLen)+")",
   238  			pb(s.put.TotalBytes())+" ("+pb(t.put.TotalBytes())+")",
   239  			pl(s.put.MinLatency(), s.put.AvgLatency(), s.put.MaxLatency()),
   240  			ps(s.put.Throughput(s.put.Start(), time.Now()))+" ("+ps(t.put.Throughput(t.put.Start(), time.Now()))+")",
   241  			errs)
   242  	}
   243  	errs = "-"
   244  	if t.get.TotalErrs() != 0 {
   245  		errs = pn(s.get.TotalErrs()) + " (" + pn(t.get.TotalErrs()) + ")"
   246  	}
   247  	if s.get.Total() != 0 {
   248  		p(to, statsPrintHeader, pt(), "GET",
   249  			pn(s.get.Total())+" ("+pn(t.get.Total())+" "+pn(getPending)+" "+pn(workOrderResLen)+")",
   250  			pb(s.get.TotalBytes())+" ("+pb(t.get.TotalBytes())+")",
   251  			pl(s.get.MinLatency(), s.get.AvgLatency(), s.get.MaxLatency()),
   252  			ps(s.get.Throughput(s.get.Start(), time.Now()))+" ("+ps(t.get.Throughput(t.get.Start(), time.Now()))+")",
   253  			errs)
   254  	}
   255  	if s.getConfig.Total() != 0 {
   256  		p(to, statsPrintHeader, pt(), "CFG",
   257  			pn(s.getConfig.Total())+" ("+pn(t.getConfig.Total())+")",
   258  			pb(s.getConfig.TotalBytes())+" ("+pb(t.getConfig.TotalBytes())+")",
   259  			pl(s.getConfig.MinLatency(), s.getConfig.AvgLatency(), s.getConfig.MaxLatency()),
   260  			ps(s.getConfig.Throughput(s.getConfig.Start(), time.Now()))+" ("+ps(t.getConfig.Throughput(t.getConfig.Start(), time.Now()))+")",
   261  			pn(s.getConfig.TotalErrs())+" ("+pn(t.getConfig.TotalErrs())+")")
   262  	}
   263  }
   264  
   265  func writeHumanReadibleFinalStats(to io.Writer, t *sts) {
   266  	p := fprintf
   267  	pn := prettyNumber
   268  	pb := prettyBytes
   269  	ps := prettySpeed
   270  	pl := prettyLatency
   271  	pt := now
   272  	preWriteStats(to, false)
   273  
   274  	sput := &t.put
   275  	if sput.Total() > 0 {
   276  		p(to, statsPrintHeader, pt(), "PUT",
   277  			pn(sput.Total()),
   278  			pb(sput.TotalBytes()),
   279  			pl(sput.MinLatency(), sput.AvgLatency(), sput.MaxLatency()),
   280  			ps(sput.Throughput(sput.Start(), time.Now())),
   281  			pn(sput.TotalErrs()))
   282  	}
   283  	sget := &t.get
   284  	if sget.Total() > 0 {
   285  		p(to, statsPrintHeader, pt(), "GET",
   286  			pn(sget.Total()),
   287  			pb(sget.TotalBytes()),
   288  			pl(sget.MinLatency(), sget.AvgLatency(), sget.MaxLatency()),
   289  			ps(sget.Throughput(sget.Start(), time.Now())),
   290  			pn(sget.TotalErrs()))
   291  	}
   292  	sconfig := &t.getConfig
   293  	if sconfig.Total() > 0 {
   294  		p(to, statsPrintHeader, pt(), "CFG",
   295  			pn(sconfig.Total()),
   296  			pb(sconfig.TotalBytes()),
   297  			pl(sconfig.MinLatency(), sconfig.AvgLatency(), sconfig.MaxLatency()),
   298  			pb(sconfig.Throughput(sconfig.Start(), time.Now())),
   299  			pn(sconfig.TotalErrs()))
   300  	}
   301  }
   302  
   303  // writeStatus writes stats to the writter.
   304  // if final = true, writes the total; otherwise writes the interval stats
   305  func writeStats(to io.Writer, jsonFormat, final bool, s, t *sts) {
   306  	if final {
   307  		writeFinalStats(to, jsonFormat, t)
   308  	} else {
   309  		// show interval stats; some fields are shown of both interval and total, for example, gets, puts, etc
   310  		writeIntervalStats(to, jsonFormat, s, t)
   311  	}
   312  }
   313  
   314  // printRunParams show run parameters in json format
   315  func printRunParams(p *params) {
   316  	var d = p.duration.String()
   317  	if p.duration.Val == time.Duration(math.MaxInt64) {
   318  		d = "-"
   319  	}
   320  	b, err := jsoniter.MarshalIndent(struct {
   321  		Seed          int64  `json:"seed,string"`
   322  		URL           string `json:"proxy"`
   323  		Bucket        string `json:"bucket"`
   324  		Provider      string `json:"provider"`
   325  		Namespace     string `json:"namespace"`
   326  		Duration      string `json:"duration"`
   327  		MaxPutBytes   int64  `json:"PUT upper bound,string"`
   328  		PutPct        int    `json:"% PUT"`
   329  		MinSize       int64  `json:"minimum object size (bytes)"`
   330  		MaxSize       int64  `json:"maximum object size (bytes)"`
   331  		NumWorkers    int    `json:"# workers"`
   332  		StatsInterval string `json:"stats interval"`
   333  		Backing       string `json:"backed by"`
   334  		Cleanup       bool   `json:"cleanup"`
   335  	}{
   336  		Seed:          p.seed,
   337  		URL:           p.proxyURL,
   338  		Bucket:        p.bck.Name,
   339  		Provider:      p.bck.Provider,
   340  		Namespace:     p.bck.Ns.String(),
   341  		Duration:      d,
   342  		MaxPutBytes:   p.putSizeUpperBound,
   343  		PutPct:        p.putPct,
   344  		MinSize:       p.minSize,
   345  		MaxSize:       p.maxSize,
   346  		NumWorkers:    p.numWorkers,
   347  		StatsInterval: (time.Duration(runParams.statsShowInterval) * time.Second).String(),
   348  		Backing:       p.readerType,
   349  		Cleanup:       p.cleanUp.Val,
   350  	}, "", "   ")
   351  	cos.AssertNoErr(err)
   352  
   353  	fmt.Printf("Runtime configuration:\n%s\n\n", string(b))
   354  }