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 }