├── .github └── workflows │ ├── go.yml │ ├── release.yml │ └── vulncheck.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CREDITS ├── Dockerfile ├── Dockerfile.release ├── LICENSE ├── NOTICE ├── README.md ├── api └── api.go ├── arch_warp.png ├── cli ├── analyze.go ├── benchclient.go ├── benchmark.go ├── benchserver.go ├── cli.go ├── client.go ├── clientmode.go ├── cmp.go ├── complete.go ├── delete.go ├── errors.go ├── fanout.go ├── flags.go ├── generator.go ├── get.go ├── influx.go ├── list.go ├── merge.go ├── mixed.go ├── multipart.go ├── multipart_put.go ├── print.go ├── put.go ├── retention.go ├── rlimit.go ├── run.go ├── snowball.go ├── sse.go ├── stat.go ├── ui.go ├── versioned.go └── zip.go ├── go.mod ├── go.sum ├── k8s ├── README.md ├── helm │ ├── Chart.yaml │ ├── README.md │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── job.yaml │ │ ├── secret.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ └── statefulset.yaml │ └── values.yaml ├── warp-job.yaml └── warp.yaml ├── logo.png ├── main.go ├── pkg ├── aggregate │ ├── aggregate.go │ ├── collector.go │ ├── compare.go │ ├── live.go │ ├── mapasslice.go │ ├── requests.go │ ├── throughput.go │ └── ttfb.go ├── bench │ ├── analyze.go │ ├── analyze_test.go │ ├── benchmark.go │ ├── category.go │ ├── category_string.go │ ├── collector.go │ ├── compare.go │ ├── csv.go │ ├── delete.go │ ├── fanout.go │ ├── get.go │ ├── list.go │ ├── mixed.go │ ├── multipart.go │ ├── multipart_put.go │ ├── ops.go │ ├── put.go │ ├── retention.go │ ├── s3zip.go │ ├── snowball.go │ ├── stat.go │ ├── testdata │ │ └── warp-benchdata-get.csv.zst │ └── versioned.go ├── build-constants.go └── generator │ ├── generator.go │ ├── generator_test.go │ ├── options.go │ └── random.go ├── systemd └── warp.service ├── warp_logo.png └── yml-samples ├── delete.yml ├── get.yml ├── list.yml ├── mixed.yml ├── multipart.yml ├── put.yml ├── stat.yml ├── versioned.yml └── zip.yml /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Test on Go ${{ matrix.go-version }} and ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | go-version: [1.24.x] 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | steps: 20 | - name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }} 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | id: go 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v2 28 | 29 | - name: Build on ${{ matrix.os }} 30 | env: 31 | GO111MODULE: on 32 | run: go test -v -race ./... 33 | 34 | lint: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Set up Go 38 | uses: actions/setup-go@v2 39 | with: 40 | go-version: 1.24.x 41 | 42 | - name: Checkout code 43 | uses: actions/checkout@v2 44 | 45 | - name: go vet 46 | run: go vet ./... 47 | 48 | - name: go fmt 49 | run: diff <(gofmt -d .) <(printf "") 50 | 51 | - name: Lint 52 | run: | 53 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin 54 | $(go env GOPATH)/bin/golangci-lint run --timeout=5m --config ./.golangci.yml 55 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, reopened, synchronize ] 6 | branches: 7 | - 'master' 8 | 9 | jobs: 10 | goreleaser: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Checkout 15 | uses: actions/checkout@v2 16 | - 17 | name: Unshallow 18 | run: git fetch --prune --unshallow 19 | - 20 | name: Set up Go 21 | uses: actions/setup-go@v1 22 | with: 23 | go-version: 1.24.x 24 | - 25 | name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v5 27 | with: 28 | version: latest 29 | args: release --clean --skip=publish --snapshot 30 | - 31 | name: Upload Win64 Binaries 32 | uses: actions/upload-artifact@v4 33 | if: success() 34 | with: 35 | name: Warp-Snapshot-Build-Win64 36 | path: dist/warp_windows_amd64_v1 37 | - 38 | name: Upload Linux Binaries 39 | uses: actions/upload-artifact@v4 40 | if: success() 41 | with: 42 | name: Warp-Snapshot-Build-Linux-amd64 43 | path: dist/warp_linux_amd64_v1 44 | - 45 | name: Upload MacOS Binaries 46 | uses: actions/upload-artifact@v4 47 | if: success() 48 | with: 49 | name: Warp-Snapshot-Build-MacOSX-amd64 50 | path: dist/warp_darwin_amd64_v1 51 | -------------------------------------------------------------------------------- /.github/workflows/vulncheck.yml: -------------------------------------------------------------------------------- 1 | name: VulnCheck 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | - main 7 | push: 8 | branches: 9 | - master 10 | - main 11 | jobs: 12 | vulncheck: 13 | name: Analysis 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | go-version: [ 1.24.x ] 18 | steps: 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v3 21 | - uses: actions/setup-go@v3 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | check-latest: true 25 | - name: Get govulncheck 26 | run: go install golang.org/x/vuln/cmd/govulncheck@latest 27 | shell: bash 28 | - name: Run govulncheck 29 | run: govulncheck ./... 30 | shell: bash 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | /.idea 14 | warp 15 | *~ 16 | dist 17 | 18 | # zst files 19 | *.zst 20 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - durationcheck 6 | - gocritic 7 | - gomodguard 8 | - govet 9 | - ineffassign 10 | - misspell 11 | - prealloc 12 | - revive 13 | - staticcheck 14 | - unconvert 15 | - unused 16 | settings: 17 | misspell: 18 | locale: US 19 | staticcheck: 20 | checks: 21 | - all 22 | - -SA1008 23 | - -SA1019 24 | - -SA4000 25 | - -SA9004 26 | - -ST1000 27 | - -ST1005 28 | - -ST1016 29 | - -U1000 30 | - -ST1005 31 | exclusions: 32 | generated: lax 33 | rules: 34 | - path: (.+)\.go$ 35 | text: should have comment or be unexported 36 | - path: (.+)\.go$ 37 | text: should have a package comment 38 | - path: (.+)\.go$ 39 | text: error strings should not be capitalized or end with punctuation or a newline 40 | paths: 41 | - third_party$ 42 | - builtin$ 43 | - examples$ 44 | formatters: 45 | enable: 46 | - gofmt 47 | - gofumpt 48 | - goimports 49 | settings: 50 | gofumpt: 51 | extra-rules: true 52 | exclusions: 53 | generated: lax 54 | paths: 55 | - third_party$ 56 | - builtin$ 57 | - examples$ 58 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | project_name: warp 4 | 5 | before: 6 | hooks: 7 | # you may remove this if you don't use vgo 8 | - go mod tidy -compat=1.21 9 | builds: 10 | - 11 | goos: 12 | - freebsd 13 | - windows 14 | - linux 15 | - darwin 16 | goarch: 17 | - amd64 18 | - arm64 19 | ignore: 20 | - goos: windows 21 | goarch: arm64 22 | env: 23 | - CGO_ENABLED=0 24 | flags: 25 | - -trimpath 26 | - --tags=kqueue 27 | ldflags: 28 | - -s -w -X github.com/minio/warp/pkg.ReleaseTag={{.Tag}} -X github.com/minio/warp/pkg.CommitID={{.FullCommit}} -X github.com/minio/warp/pkg.Version={{.Version}} -X github.com/minio/warp/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/warp/pkg.ReleaseTime={{.Date}} 29 | archives: 30 | - 31 | name_template: >- 32 | {{ .ProjectName }}_ 33 | {{- title .Os }}_ 34 | {{- if eq .Arch "amd64" }}x86_64 35 | {{- else if eq .Arch "386" }}i386 36 | {{- else }}{{ .Arch }}{{ end }} 37 | format_overrides: 38 | - goos: windows 39 | format: zip 40 | files: 41 | - README.md 42 | - LICENSE 43 | - warp_logo.png 44 | checksum: 45 | name_template: 'checksums.txt' 46 | snapshot: 47 | name_template: 'snapshot-{{ time "2006-01-02" }}' 48 | changelog: 49 | sort: asc 50 | filters: 51 | exclude: 52 | - '^docs:' 53 | - '^test:' 54 | nfpms: 55 | - 56 | vendor: MinIO Inc. 57 | homepage: https://github.com/minio/warp 58 | maintainer: MinIO 59 | description: S3 API Benchmark Tool 60 | license: GNU Affero General Public License v3.0 61 | formats: 62 | - deb 63 | - rpm 64 | contents: 65 | # Basic file that applies to all packagers 66 | - src: systemd/warp.service 67 | dst: /etc/systemd/system/warp.service 68 | file_name_template: >- 69 | {{ .ProjectName }}_ 70 | {{- title .Os }}_ 71 | {{- if eq .Arch "amd64" }}x86_64 72 | {{- else if eq .Arch "386" }}i386 73 | {{- else }}{{ .Arch }}{{ end }} 74 | dockers: 75 | - 76 | # GOOS of the built binary that should be used. 77 | goos: linux 78 | # GOARCH of the built binary that should be used. 79 | goarch: amd64 80 | dockerfile: Dockerfile.release 81 | - image_templates: 82 | - "quay.io/minio/warp:{{ .Tag }}-amd64" 83 | use: buildx 84 | goarch: amd64 85 | ids: 86 | - warp 87 | dockerfile: Dockerfile.release 88 | build_flag_templates: 89 | - "--platform=linux/amd64" 90 | - image_templates: 91 | - "quay.io/minio/warp:{{ .Tag }}-arm64" 92 | use: buildx 93 | goarch: arm64 94 | ids: 95 | - warp 96 | dockerfile: Dockerfile.release 97 | build_flag_templates: 98 | - "--platform=linux/arm64" 99 | - image_templates: 100 | - "minio/warp:{{ .Tag }}-amd64" 101 | use: buildx 102 | goarch: amd64 103 | ids: 104 | - warp 105 | dockerfile: Dockerfile.release 106 | build_flag_templates: 107 | - "--platform=linux/amd64" 108 | - image_templates: 109 | - "minio/warp:{{ .Tag }}-arm64" 110 | use: buildx 111 | goarch: arm64 112 | ids: 113 | - warp 114 | dockerfile: Dockerfile.release 115 | build_flag_templates: 116 | - "--platform=linux/arm64" 117 | docker_manifests: 118 | - name_template: minio/warp:{{ .Tag }} 119 | image_templates: 120 | - minio/warp:{{ .Tag }}-amd64 121 | - minio/warp:{{ .Tag }}-arm64 122 | - name_template: minio/warp:latest 123 | image_templates: 124 | - minio/warp:{{ .Tag }}-amd64 125 | - minio/warp:{{ .Tag }}-arm64 126 | - name_template: quay.io/minio/warp:{{ .Tag }} 127 | image_templates: 128 | - quay.io/minio/warp:{{ .Tag }}-amd64 129 | - quay.io/minio/warp:{{ .Tag }}-arm64 130 | - name_template: quay.io/minio/warp:latest 131 | image_templates: 132 | - quay.io/minio/warp:{{ .Tag }}-amd64 133 | - quay.io/minio/warp:{{ .Tag }}-arm64 134 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 2 | 3 | ADD go.mod /go/src/github.com/minio/warp/go.mod 4 | ADD go.sum /go/src/github.com/minio/warp/go.sum 5 | WORKDIR /go/src/github.com/minio/warp/ 6 | # Get dependencies - will also be cached if we won't change mod/sum 7 | RUN go mod download 8 | 9 | ADD . /go/src/github.com/minio/warp/ 10 | WORKDIR /go/src/github.com/minio/warp/ 11 | 12 | ENV CGO_ENABLED=0 13 | 14 | RUN go build -ldflags '-w -s' -a -o warp . 15 | 16 | FROM alpine 17 | MAINTAINER MinIO Development "dev@min.io" 18 | EXPOSE 7761 19 | 20 | COPY --from=0 /go/src/github.com/minio/warp/warp /warp 21 | 22 | ENTRYPOINT ["/warp"] 23 | -------------------------------------------------------------------------------- /Dockerfile.release: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | MAINTAINER MinIO Development "dev@min.io" 3 | EXPOSE 7761 4 | COPY warp /warp 5 | 6 | ENTRYPOINT ["/warp"] 7 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | MinIO Warp benchmark (C) 2019-2020 MinIO, Inc. 2 | 3 | This product includes software developed at MinIO, Inc. 4 | (https://min.io/). 5 | 6 | The MinIO project contains unmodified/modified subcomponents too with 7 | separate copyright notices and license terms. Your use of the source 8 | code for the these subcomponents is subject to the terms and conditions 9 | of the following licenses. 10 | -------------------------------------------------------------------------------- /arch_warp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minio/warp/4736827d64c59b604f64fb332a062da769150441/arch_warp.png -------------------------------------------------------------------------------- /cli/clientmode.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "net/http" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/minio/cli" 26 | "github.com/minio/mc/pkg/probe" 27 | "github.com/minio/pkg/v3/console" 28 | ) 29 | 30 | var clientFlags = []cli.Flag{} 31 | 32 | // Put command. 33 | var clientCmd = cli.Command{ 34 | Name: "client", 35 | Usage: "run warp in client mode, accepting connections to run benchmarks", 36 | Action: mainClient, 37 | Before: setGlobalsFromContext, 38 | Flags: combineFlags(globalFlags, clientFlags), 39 | CustomHelpTemplate: `NAME: 40 | {{.HelpName}} - {{.Usage}} 41 | 42 | USAGE: 43 | {{.HelpName}} [FLAGS] [listen address] 44 | -> see https://github.com/minio/warp#multiple-hosts 45 | 46 | FLAGS: 47 | {{range .VisibleFlags}}{{.}} 48 | {{end}} 49 | 50 | EXAMPLES: 51 | 1. Listen on port '6001' with ip 192.168.1.101: 52 | {{.Prompt}} {{.HelpName}} 192.168.1.101:6001 53 | `, 54 | } 55 | 56 | const warpServerDefaultPort = 7761 57 | 58 | // mainPut is the entry point for cp command. 59 | func mainClient(ctx *cli.Context) error { 60 | checkClientSyntax(ctx) 61 | addr := ":" + strconv.Itoa(warpServerDefaultPort) 62 | switch ctx.NArg() { 63 | case 1: 64 | addr = ctx.Args()[0] 65 | if !strings.Contains(addr, ":") { 66 | addr += ":" + strconv.Itoa(warpServerDefaultPort) 67 | } 68 | case 0: 69 | default: 70 | fatal(errInvalidArgument(), "Too many parameters") 71 | } 72 | http.HandleFunc("/ws", serveWs) 73 | console.Infoln("Listening on", addr, "Press Ctrl+C to exit.") 74 | fatalIf(probe.NewError(http.ListenAndServe(addr, nil)), "Unable to start client") 75 | return nil 76 | } 77 | 78 | func checkClientSyntax(_ *cli.Context) { 79 | } 80 | -------------------------------------------------------------------------------- /cli/complete.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | 25 | "github.com/minio/cli" 26 | "github.com/posener/complete" 27 | ) 28 | 29 | // Main function to answer to bash completion calls 30 | func mainComplete() error { 31 | // Recursively register all commands and subcommands 32 | // along with global and local flags 33 | complCmds := make(complete.Commands) 34 | for _, cmd := range appCmds { 35 | complCmds[cmd.Name] = cmdToCompleteCmd(cmd, "") 36 | } 37 | complFlags := flagsToCompleteFlags(nil) 38 | cliComplete := complete.Command{ 39 | Sub: complCmds, 40 | GlobalFlags: complFlags, 41 | } 42 | // Answer to bash completion call 43 | complete.New(filepath.Base(os.Args[0]), cliComplete).Run() 44 | return nil 45 | } 46 | 47 | // The list of all commands supported by warp with their mapping 48 | // with their bash completer function 49 | var completeCmds = map[string]complete.Predictor{ 50 | "/version": nil, 51 | } 52 | 53 | // flagsToCompleteFlags transforms a cli.Flag to complete.Flags 54 | // understood by posener/complete library. 55 | func flagsToCompleteFlags(flags []cli.Flag) complete.Flags { 56 | complFlags := make(complete.Flags) 57 | for _, f := range flags { 58 | for _, s := range strings.Split(f.GetName(), ",") { 59 | var flagName string 60 | s = strings.TrimSpace(s) 61 | if len(s) == 1 { 62 | flagName = "-" + s 63 | } else { 64 | flagName = "--" + s 65 | } 66 | complFlags[flagName] = complete.PredictNothing 67 | } 68 | } 69 | return complFlags 70 | } 71 | 72 | // This function recursively transforms cli.Command to complete.Command 73 | // understood by posener/complete library. 74 | func cmdToCompleteCmd(cmd cli.Command, parentPath string) complete.Command { 75 | var complCmd complete.Command 76 | complCmd.Sub = make(complete.Commands) 77 | 78 | for _, subCmd := range cmd.Subcommands { 79 | complCmd.Sub[subCmd.Name] = cmdToCompleteCmd(subCmd, parentPath+"/"+cmd.Name) 80 | } 81 | 82 | complCmd.Flags = flagsToCompleteFlags(cmd.Flags) 83 | complCmd.Args = completeCmds[parentPath+"/"+cmd.Name] 84 | return complCmd 85 | } 86 | -------------------------------------------------------------------------------- /cli/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "github.com/minio/cli" 22 | "github.com/minio/pkg/v3/console" 23 | "github.com/minio/warp/pkg/bench" 24 | ) 25 | 26 | var deleteFlags = []cli.Flag{ 27 | cli.IntFlag{ 28 | Name: "objects", 29 | Value: 25000, 30 | Usage: "Number of objects to upload.", 31 | }, 32 | cli.StringFlag{ 33 | Name: "obj.size", 34 | Value: "1KiB", 35 | Usage: "Size of each generated object. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 36 | }, 37 | cli.IntFlag{ 38 | Name: "batch", 39 | Value: 100, 40 | Usage: "Number of DELETE operations per batch.", 41 | }, 42 | cli.BoolFlag{ 43 | Name: "list-existing", 44 | Usage: "Instead of preparing the bench by PUTing some objects, only use objects already in the bucket", 45 | }, 46 | cli.BoolFlag{ 47 | Name: "list-flat", 48 | Usage: "When using --list-existing, do not use recursive listing", 49 | }, 50 | } 51 | 52 | var DeletedCombinedFlags = combineFlags(globalFlags, ioFlags, deleteFlags, genFlags, benchFlags, analyzeFlags) 53 | 54 | var deleteCmd = cli.Command{ 55 | Name: "delete", 56 | Usage: "benchmark delete objects", 57 | Action: mainDelete, 58 | Before: setGlobalsFromContext, 59 | Flags: DeletedCombinedFlags, 60 | CustomHelpTemplate: `NAME: 61 | {{.HelpName}} - {{.Usage}} 62 | 63 | The benchmark will end when either all objects have been deleted or the durations specified with -duration has been reached. 64 | USAGE: 65 | {{.HelpName}} [FLAGS] 66 | -> see https://github.com/minio/warp#delete 67 | 68 | FLAGS: 69 | {{range .VisibleFlags}}{{.}} 70 | {{end}}`, 71 | } 72 | 73 | // mainDelete is the entry point for get command. 74 | func mainDelete(ctx *cli.Context) error { 75 | checkDeleteSyntax(ctx) 76 | 77 | b := bench.Delete{ 78 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 79 | CreateObjects: ctx.Int("objects"), 80 | BatchSize: ctx.Int("batch"), 81 | ListExisting: ctx.Bool("list-existing"), 82 | ListFlat: ctx.Bool("list-flat"), 83 | ListPrefix: ctx.String("prefix"), 84 | } 85 | if b.ListExisting && !ctx.IsSet("objects") { 86 | b.CreateObjects = 0 87 | } 88 | return runBench(ctx, &b) 89 | } 90 | 91 | func checkDeleteSyntax(ctx *cli.Context) { 92 | if ctx.NArg() > 0 { 93 | console.Fatal("Command takes no arguments") 94 | } 95 | checkAnalyze(ctx) 96 | checkBenchmark(ctx) 97 | if ctx.Int("batch") < 1 { 98 | console.Fatal("batch size much be 1 or bigger") 99 | } 100 | if !ctx.Bool("list-existing") { 101 | wantO := ctx.Int("batch") * ctx.Int("concurrent") * 4 102 | if ctx.Int("objects") < wantO { 103 | console.Fatalf("Too few objects: With current --batch and --concurrent settings, at least %d objects should be used for a valid benchmark. Use --objects=%d", wantO, wantO) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /cli/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/minio/mc/pkg/probe" 24 | ) 25 | 26 | type dummyErr error 27 | 28 | var errDummy = func() *probe.Error { 29 | msg := "" 30 | return probe.NewError(dummyErr(errors.New(msg))).Untrace() 31 | } 32 | 33 | type invalidArgumentErr error 34 | 35 | var errInvalidArgument = func() *probe.Error { 36 | msg := "Invalid arguments provided, please refer " + "`" + appName + " -h` for relevant documentation." 37 | return probe.NewError(invalidArgumentErr(errors.New(msg))).Untrace() 38 | } 39 | -------------------------------------------------------------------------------- /cli/fanout.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2023 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "github.com/minio/cli" 22 | "github.com/minio/pkg/v3/console" 23 | "github.com/minio/warp/pkg/bench" 24 | ) 25 | 26 | var fanoutFlags = []cli.Flag{ 27 | cli.StringFlag{ 28 | Name: "obj.size", 29 | Value: "1MiB", 30 | Usage: "Size of each generated object. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 31 | }, 32 | cli.IntFlag{ 33 | Name: "copies", 34 | Value: 100, 35 | Usage: "Number of copies per uploaded object", 36 | Hidden: true, 37 | }, 38 | } 39 | 40 | // Fanout command. 41 | var fanoutCmd = cli.Command{ 42 | Name: "fanout", 43 | Usage: "benchmark fan-out of objects on MinIO servers", 44 | Action: mainFanout, 45 | Before: setGlobalsFromContext, 46 | Flags: combineFlags(globalFlags, ioFlags, fanoutFlags, genFlags, benchFlags, analyzeFlags), 47 | CustomHelpTemplate: `NAME: 48 | {{.HelpName}} - {{.Usage}} 49 | 50 | USAGE: 51 | {{.HelpName}} [FLAGS] 52 | -> see https://github.com/minio/warp#fanout 53 | 54 | FLAGS: 55 | {{range .VisibleFlags}}{{.}} 56 | {{end}}`, 57 | } 58 | 59 | // mainFanout is the entry point for cp command. 60 | func mainFanout(ctx *cli.Context) error { 61 | checkFanoutSyntax(ctx) 62 | b := bench.Fanout{ 63 | Copies: ctx.Int("copies"), 64 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 65 | } 66 | return runBench(ctx, &b) 67 | } 68 | 69 | func checkFanoutSyntax(ctx *cli.Context) { 70 | if ctx.NArg() > 0 { 71 | console.Fatal("Command takes no arguments") 72 | } 73 | if ctx.Int("copies") <= 0 { 74 | console.Fatal("Copies must be bigger than 0") 75 | } 76 | checkAnalyze(ctx) 77 | checkBenchmark(ctx) 78 | } 79 | -------------------------------------------------------------------------------- /cli/generator.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/dustin/go-humanize" 26 | "github.com/minio/mc/pkg/probe" 27 | 28 | "github.com/minio/cli" 29 | "github.com/minio/warp/pkg/generator" 30 | 31 | hist "github.com/jfsmig/prng/histogram" 32 | ) 33 | 34 | var genFlags = []cli.Flag{ 35 | cli.StringFlag{ 36 | Name: "obj.generator", 37 | Value: "random", 38 | Usage: "Use specific data generator", 39 | }, 40 | cli.BoolFlag{ 41 | Name: "obj.randsize", 42 | Usage: "Randomize size of objects so they will be up to the specified size", 43 | }, 44 | } 45 | 46 | // newGenSource returns a new generator 47 | func newGenSource(ctx *cli.Context, sizeField string) func() generator.Source { 48 | prefixSize := 8 49 | if ctx.Bool("noprefix") { 50 | prefixSize = 0 51 | } 52 | 53 | var g generator.OptionApplier 54 | switch ctx.String("obj.generator") { 55 | case "random": 56 | g = generator.WithRandomData() 57 | default: 58 | err := errors.New("unknown generator type:" + ctx.String("obj.generator")) 59 | fatal(probe.NewError(err), "Invalid -generator parameter") 60 | return nil 61 | } 62 | opts := []generator.Option{ 63 | generator.WithCustomPrefix(ctx.String("prefix")), 64 | generator.WithPrefixSize(prefixSize), 65 | } 66 | if strings.IndexRune(ctx.String(sizeField), ':') > 0 { 67 | if _, err := hist.ParseCSV(ctx.String(sizeField)); err != nil { 68 | fatalIf(probe.NewError(err), "Invalid histogram format for the size parameter") 69 | } else { 70 | opts = append(opts, generator.WithSizeHistograms(ctx.String(sizeField))) 71 | } 72 | } else { 73 | tokens := strings.Split(ctx.String(sizeField), ",") 74 | switch len(tokens) { 75 | case 1: 76 | size, err := toSize(tokens[0]) 77 | if err != nil { 78 | fatalIf(probe.NewError(err), "Invalid obj.size specified") 79 | } 80 | opts = append(opts, generator.WithSize(int64(size))) 81 | case 2: 82 | minSize, err := toSize(tokens[0]) 83 | if err != nil { 84 | fatalIf(probe.NewError(err), "Invalid min obj.size specified") 85 | } 86 | maxSize, err := toSize(tokens[1]) 87 | if err != nil { 88 | fatalIf(probe.NewError(err), "Invalid max obj.size specified") 89 | } 90 | opts = append(opts, generator.WithMinMaxSize(int64(minSize), int64(maxSize))) 91 | default: 92 | fatalIf(probe.NewError(fmt.Errorf("unexpected obj.size specified: %s", ctx.String(sizeField))), "Invalid obj.size parameter") 93 | } 94 | 95 | opts = append([]generator.Option{g.Apply()}, append(opts, generator.WithRandomSize(ctx.Bool("obj.randsize")))...) 96 | } 97 | 98 | src, err := generator.NewFn(opts...) 99 | fatalIf(probe.NewError(err), "Unable to create data generator") 100 | return src 101 | } 102 | 103 | // toSize converts a size indication to bytes. 104 | func toSize(size string) (uint64, error) { 105 | return humanize.ParseBytes(size) 106 | } 107 | -------------------------------------------------------------------------------- /cli/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "github.com/minio/cli" 22 | "github.com/minio/minio-go/v7" 23 | "github.com/minio/pkg/v3/console" 24 | "github.com/minio/warp/pkg/bench" 25 | ) 26 | 27 | var getFlags = []cli.Flag{ 28 | cli.IntFlag{ 29 | Name: "objects", 30 | Value: 2500, 31 | Usage: "Number of objects to upload.", 32 | }, 33 | cli.StringFlag{ 34 | Name: "obj.size", 35 | Value: "10MiB", 36 | Usage: "Size of each generated object. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 37 | }, 38 | cli.StringFlag{ 39 | Name: "part.size", 40 | Value: "", 41 | Usage: "Multipart part size. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 42 | Hidden: true, 43 | }, 44 | cli.BoolFlag{ 45 | Name: "range", 46 | Usage: "Do ranged get operations. Will request with random offset and length.", 47 | }, 48 | cli.StringFlag{ 49 | Name: "range-size", 50 | Usage: "Use a fixed range size while doing random range offsets, --range is implied", 51 | }, 52 | cli.IntFlag{ 53 | Name: "versions", 54 | Value: 1, 55 | Usage: "Number of versions to upload. If more than 1, versioned listing will be benchmarked", 56 | }, 57 | cli.BoolFlag{ 58 | Name: "list-existing", 59 | Usage: "Instead of preparing the bench by PUTing some objects, only use objects already in the bucket", 60 | }, 61 | cli.BoolFlag{ 62 | Name: "list-flat", 63 | Usage: "When using --list-existing, do not use recursive listing", 64 | }, 65 | } 66 | 67 | var GetCombinedFlags = combineFlags(globalFlags, ioFlags, getFlags, genFlags, benchFlags, analyzeFlags) 68 | 69 | var getCmd = cli.Command{ 70 | Name: "get", 71 | Usage: "benchmark get objects", 72 | Action: mainGet, 73 | Before: setGlobalsFromContext, 74 | Flags: GetCombinedFlags, 75 | CustomHelpTemplate: `NAME: 76 | {{.HelpName}} - {{.Usage}} 77 | 78 | USAGE: 79 | {{.HelpName}} [FLAGS] 80 | -> see https://github.com/minio/warp#get 81 | 82 | FLAGS: 83 | {{range .VisibleFlags}}{{.}} 84 | {{end}}`, 85 | } 86 | 87 | // mainGet is the entry point for get command. 88 | func mainGet(ctx *cli.Context) error { 89 | checkGetSyntax(ctx) 90 | 91 | var rangeSize int64 92 | if rs := ctx.String("range-size"); rs != "" { 93 | s, err := toSize(rs) 94 | if err != nil { 95 | return err 96 | } 97 | rangeSize = int64(s) 98 | } 99 | 100 | sse := newSSE(ctx) 101 | b := bench.Get{ 102 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 103 | Versions: ctx.Int("versions"), 104 | RandomRanges: ctx.Bool("range") || ctx.IsSet("range-size"), 105 | RangeSize: rangeSize, 106 | CreateObjects: ctx.Int("objects"), 107 | GetOpts: minio.GetObjectOptions{ServerSideEncryption: sse}, 108 | ListExisting: ctx.Bool("list-existing"), 109 | ListFlat: ctx.Bool("list-flat"), 110 | ListPrefix: ctx.String("prefix"), 111 | } 112 | return runBench(ctx, &b) 113 | } 114 | 115 | func checkGetSyntax(ctx *cli.Context) { 116 | if ctx.NArg() > 0 { 117 | console.Fatal("Command takes no arguments") 118 | } 119 | if ctx.Int("versions") < 1 { 120 | console.Fatal("At least one version must be tested") 121 | } 122 | if ctx.Int("objects") < 1 { 123 | console.Fatal("At least one object must be tested") 124 | } 125 | checkAnalyze(ctx) 126 | checkBenchmark(ctx) 127 | } 128 | -------------------------------------------------------------------------------- /cli/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "github.com/minio/cli" 22 | "github.com/minio/pkg/v3/console" 23 | "github.com/minio/warp/pkg/bench" 24 | ) 25 | 26 | var listFlags = []cli.Flag{ 27 | cli.IntFlag{ 28 | Name: "objects", 29 | Value: 10000, 30 | Usage: "Number of objects to upload. Rounded up to have equal concurrent objects.", 31 | }, 32 | cli.IntFlag{ 33 | Name: "versions", 34 | Value: 1, 35 | Usage: "Number of versions to upload. If more than 1, versioned listing will be benchmarked", 36 | }, 37 | cli.StringFlag{ 38 | Name: "obj.size", 39 | Value: "1KB", 40 | Usage: "Size of each generated object. Can be a number or 10KB/MB/GB. All sizes are base 2 binary.", 41 | }, 42 | cli.BoolFlag{ 43 | Name: "metadata", 44 | Usage: "Enable extended MinIO ListObjects with metadata, by default this benchmarking uses ListObjectsV2 API.", 45 | }, 46 | cli.IntFlag{ 47 | Name: "max-keys", 48 | Value: 100, 49 | Usage: "Set the number of keys requested per list request.", 50 | }, 51 | } 52 | 53 | var ListCombinedFlags = combineFlags(globalFlags, ioFlags, listFlags, genFlags, benchFlags, analyzeFlags) 54 | 55 | var listCmd = cli.Command{ 56 | Name: "list", 57 | Usage: "benchmark list objects", 58 | Action: mainList, 59 | Before: setGlobalsFromContext, 60 | Flags: ListCombinedFlags, 61 | CustomHelpTemplate: `NAME: 62 | {{.HelpName}} - {{.Usage}} 63 | 64 | USAGE: 65 | {{.HelpName}} [FLAGS] 66 | -> see https://github.com/minio/warp#list 67 | 68 | FLAGS: 69 | {{range .VisibleFlags}}{{.}} 70 | {{end}}`, 71 | } 72 | 73 | // mainDelete is the entry point for get command. 74 | func mainList(ctx *cli.Context) error { 75 | checkListSyntax(ctx) 76 | 77 | b := bench.List{ 78 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 79 | Versions: ctx.Int("versions"), 80 | Metadata: ctx.Bool("metadata"), 81 | CreateObjects: ctx.Int("objects"), 82 | NoPrefix: ctx.Bool("noprefix"), 83 | MaxKeys: ctx.Int("max-keys"), 84 | } 85 | return runBench(ctx, &b) 86 | } 87 | 88 | func checkListSyntax(ctx *cli.Context) { 89 | if ctx.NArg() > 0 { 90 | console.Fatal("Command takes no arguments") 91 | } 92 | if ctx.Int("versions") < 1 { 93 | console.Fatal("At least one version must be tested") 94 | } 95 | if ctx.Int("objects") < 1 { 96 | console.Fatal("At least one object must be tested") 97 | } 98 | if ctx.Int("max-keys") < 1 { 99 | console.Fatal("Max keys must be at least 1.") 100 | } 101 | if ctx.Int("max-keys") > 5000 { 102 | console.Fatal("Max keys cannot be greater than 5000") 103 | } 104 | checkAnalyze(ctx) 105 | checkBenchmark(ctx) 106 | } 107 | -------------------------------------------------------------------------------- /cli/merge.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "os" 24 | "time" 25 | 26 | "github.com/klauspost/compress/zstd" 27 | "github.com/minio/cli" 28 | "github.com/minio/mc/pkg/probe" 29 | "github.com/minio/pkg/v3/console" 30 | "github.com/minio/warp/pkg/bench" 31 | ) 32 | 33 | var mergeFlags = []cli.Flag{ 34 | cli.StringFlag{ 35 | Name: "benchdata", 36 | Value: "", 37 | Usage: "Output combined data to this file. By default unique filename is generated.", 38 | }, 39 | } 40 | 41 | var mergeCmd = cli.Command{ 42 | Name: "merge", 43 | Usage: "merge existing benchmark data", 44 | Action: mainMerge, 45 | Before: setGlobalsFromContext, 46 | Flags: combineFlags(globalFlags, mergeFlags), 47 | CustomHelpTemplate: `NAME: 48 | {{.HelpName}} - {{.Usage}} 49 | 50 | USAGE: 51 | {{.HelpName}} [FLAGS] benchmark-data-file1 benchmark-data-file2 ... 52 | -> see https://github.com/minio/warp#merging-benchmarks 53 | 54 | FLAGS: 55 | {{range .VisibleFlags}}{{.}} 56 | {{end}}`, 57 | } 58 | 59 | // mainAnalyze is the entry point for analyze command. 60 | func mainMerge(ctx *cli.Context) error { 61 | checkMerge(ctx) 62 | args := ctx.Args() 63 | if len(args) <= 1 { 64 | console.Fatal("Two or more benchmark data files must be supplied") 65 | } 66 | zstdDec, _ := zstd.NewReader(nil) 67 | defer zstdDec.Close() 68 | var allOps bench.Operations 69 | threads := uint16(0) 70 | log := console.Printf 71 | if globalQuiet { 72 | log = nil 73 | } 74 | for _, arg := range args { 75 | f, err := os.Open(arg) 76 | fatalIf(probe.NewError(err), "Unable to open input file") 77 | defer f.Close() 78 | err = zstdDec.Reset(f) 79 | fatalIf(probe.NewError(err), "Unable to decompress input") 80 | ops, err := bench.OperationsFromCSV(zstdDec, false, ctx.Int("analyze.offset"), ctx.Int("analyze.limit"), log) 81 | fatalIf(probe.NewError(err), "Unable to parse input") 82 | 83 | threads = ops.OffsetThreads(threads) 84 | allOps = append(allOps, ops...) 85 | } 86 | if len(allOps) == 0 { 87 | return errors.New("benchmark files contains no data") 88 | } 89 | fileName := ctx.String("benchdata") 90 | if fileName == "" { 91 | fileName = fmt.Sprintf("%s-%s-%s", appName, ctx.Command.Name, time.Now().Format("2006-01-02[150405]")) 92 | } 93 | if len(allOps) > 0 { 94 | allOps.SortByStartTime() 95 | f, err := os.Create(fileName + ".csv.zst") 96 | if err != nil { 97 | console.Error("Unable to write benchmark data:", err) 98 | } else { 99 | func() { 100 | defer f.Close() 101 | enc, err := zstd.NewWriter(f, zstd.WithEncoderLevel(zstd.SpeedBetterCompression)) 102 | fatalIf(probe.NewError(err), "Unable to compress benchmark output") 103 | 104 | defer enc.Close() 105 | err = allOps.CSV(enc, commandLine(ctx)) 106 | fatalIf(probe.NewError(err), "Unable to write benchmark output") 107 | 108 | console.Infof("Benchmark data written to %q\n", fileName+".csv.zst") 109 | }() 110 | } 111 | } 112 | for typ, ops := range allOps.SortSplitByOpType() { 113 | start, end := ops.ActiveTimeRange(true) 114 | if !start.Before(end) { 115 | console.Errorf("Type %v contains no overlapping items", typ) 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | func checkMerge(_ *cli.Context) { 122 | } 123 | -------------------------------------------------------------------------------- /cli/mixed.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "net/http" 22 | 23 | "github.com/minio/cli" 24 | "github.com/minio/mc/pkg/probe" 25 | "github.com/minio/minio-go/v7" 26 | "github.com/minio/pkg/v3/console" 27 | "github.com/minio/warp/pkg/bench" 28 | ) 29 | 30 | var mixedFlags = []cli.Flag{ 31 | cli.IntFlag{ 32 | Name: "objects", 33 | Value: 2500, 34 | Usage: "Number of objects to upload.", 35 | }, 36 | cli.StringFlag{ 37 | Name: "obj.size", 38 | Value: "10MiB", 39 | Usage: "Size of each generated object. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 40 | }, 41 | cli.Float64Flag{ 42 | Name: "get-distrib", 43 | Usage: "The amount of GET operations.", 44 | Value: 45, 45 | }, 46 | cli.Float64Flag{ 47 | Name: "stat-distrib", 48 | Usage: "The amount of STAT operations.", 49 | Value: 30, 50 | }, 51 | cli.Float64Flag{ 52 | Name: "put-distrib", 53 | Usage: "The amount of PUT operations.", 54 | Value: 15, 55 | }, 56 | cli.Float64Flag{ 57 | Name: "delete-distrib", 58 | Usage: "The amount of DELETE operations. Must be same or lower than -put-distrib", 59 | Value: 10, 60 | }, 61 | } 62 | 63 | var MixedCombinedFlags = combineFlags(globalFlags, ioFlags, mixedFlags, genFlags, benchFlags, analyzeFlags) 64 | 65 | var mixedCmd = cli.Command{ 66 | Name: "mixed", 67 | Usage: "benchmark mixed objects", 68 | Action: mainMixed, 69 | Before: setGlobalsFromContext, 70 | Flags: MixedCombinedFlags, 71 | CustomHelpTemplate: `NAME: 72 | {{.HelpName}} - {{.Usage}} 73 | 74 | USAGE: 75 | {{.HelpName}} [FLAGS] 76 | -> see https://github.com/minio/warp#mixed 77 | 78 | FLAGS: 79 | {{range .VisibleFlags}}{{.}} 80 | {{end}}`, 81 | } 82 | 83 | // mainMixed is the entry point for mixed command. 84 | func mainMixed(ctx *cli.Context) error { 85 | checkMixedSyntax(ctx) 86 | sse := newSSE(ctx) 87 | dist := bench.MixedDistribution{ 88 | Distribution: map[string]float64{ 89 | http.MethodGet: ctx.Float64("get-distrib"), 90 | "STAT": ctx.Float64("stat-distrib"), 91 | http.MethodPut: ctx.Float64("put-distrib"), 92 | http.MethodDelete: ctx.Float64("delete-distrib"), 93 | }, 94 | } 95 | err := dist.Generate(ctx.Int("objects") * 2) 96 | fatalIf(probe.NewError(err), "Invalid distribution") 97 | b := bench.Mixed{ 98 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 99 | CreateObjects: ctx.Int("objects"), 100 | GetOpts: minio.GetObjectOptions{ServerSideEncryption: sse}, 101 | StatOpts: minio.StatObjectOptions{ 102 | ServerSideEncryption: sse, 103 | }, 104 | Dist: &dist, 105 | } 106 | return runBench(ctx, &b) 107 | } 108 | 109 | func checkMixedSyntax(ctx *cli.Context) { 110 | if ctx.NArg() > 0 { 111 | console.Fatal("Command takes no arguments") 112 | } 113 | if ctx.Int("objects") < 1 { 114 | console.Fatal("At least one object must be tested") 115 | } 116 | checkAnalyze(ctx) 117 | checkBenchmark(ctx) 118 | } 119 | -------------------------------------------------------------------------------- /cli/multipart.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2022 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "context" 22 | 23 | "github.com/minio/cli" 24 | "github.com/minio/minio-go/v7" 25 | "github.com/minio/pkg/v3/console" 26 | "github.com/minio/warp/pkg/bench" 27 | ) 28 | 29 | var multipartFlags = []cli.Flag{ 30 | cli.StringFlag{ 31 | Name: "part.size", 32 | Value: "5MiB", 33 | Usage: "Size of each part. Can be a number or MiB/GiB. Must be >= 5MiB", 34 | }, 35 | cli.IntFlag{ 36 | Name: "parts", 37 | Value: 200, 38 | Usage: "Parts to add per client", 39 | }, 40 | cli.StringFlag{ 41 | Name: "obj.name", 42 | Value: "warp-multipart.bin", 43 | Usage: "Object name.", 44 | }, 45 | cli.StringFlag{ 46 | Name: "_upload-id", 47 | Value: "", 48 | Usage: "(internal)", 49 | Hidden: true, 50 | }, 51 | cli.IntFlag{ 52 | Name: "_part-start", 53 | Value: 1, 54 | Usage: "(internal)", 55 | Hidden: true, 56 | }, 57 | } 58 | 59 | var MultiPartCombinedFlags = combineFlags(globalFlags, ioFlags, multipartFlags, genFlags, benchFlags, analyzeFlags) 60 | 61 | // MultiPart command. 62 | var multipartCmd = cli.Command{ 63 | Name: "multipart", 64 | Usage: "benchmark multipart object", 65 | Action: mainMultipart, 66 | Before: setGlobalsFromContext, 67 | Flags: MultiPartCombinedFlags, 68 | CustomHelpTemplate: `NAME: 69 | {{.HelpName}} - {{.Usage}} 70 | 71 | USAGE: 72 | {{.HelpName}} [FLAGS] 73 | -> see https://github.com/minio/warp#put 74 | 75 | FLAGS: 76 | {{range .VisibleFlags}}{{.}} 77 | {{end}}`, 78 | } 79 | 80 | // mainMultipart is the entry point for put command. 81 | func mainMultipart(ctx *cli.Context) error { 82 | checkMultipartSyntax(ctx) 83 | b := bench.Multipart{ 84 | Common: getCommon(ctx, newGenSource(ctx, "part.size")), 85 | ObjName: ctx.String("obj.name"), 86 | PartStart: ctx.Int("_part-start"), 87 | UploadID: ctx.String("_upload-id"), 88 | CreateParts: ctx.Int("parts"), 89 | } 90 | b.PutOpts = multipartOpts(ctx) 91 | if b.UploadID == "" { 92 | err := b.InitOnce(context.Background()) 93 | if err != nil { 94 | console.Fatal(err) 95 | } 96 | b.ExtraFlags = map[string]string{"_upload-id": b.UploadID, "noprefix": "true"} 97 | } 98 | return runBench(ctx, &b) 99 | } 100 | 101 | // multipartOpts retrieves put options from the context. 102 | func multipartOpts(ctx *cli.Context) minio.PutObjectOptions { 103 | return minio.PutObjectOptions{ 104 | ServerSideEncryption: newSSE(ctx), 105 | DisableMultipart: false, 106 | DisableContentSha256: ctx.Bool("disable-sha256-payload"), 107 | SendContentMd5: ctx.Bool("md5"), 108 | StorageClass: ctx.String("storage-class"), 109 | } 110 | } 111 | 112 | func checkMultipartSyntax(ctx *cli.Context) { 113 | if ctx.NArg() > 0 { 114 | console.Fatal("Command takes no arguments") 115 | } 116 | if ctx.Bool("disable-multipart") { 117 | console.Fatal("Cannot disable multipart for multipart test") 118 | } 119 | if ctx.Int("parts") <= 0 { 120 | console.Fatal("part.count must be > 1") 121 | } 122 | if sz, err := toSize(ctx.String("part.size")); sz < 5<<20 { 123 | if err != nil { 124 | console.Fatal("error parsing part.size:", err) 125 | } 126 | if sz < 5<<20 { 127 | console.Fatal("part.size must be >= 5MiB") 128 | } 129 | } 130 | 131 | checkAnalyze(ctx) 132 | checkBenchmark(ctx) 133 | } 134 | -------------------------------------------------------------------------------- /cli/multipart_put.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/minio/cli" 5 | "github.com/minio/pkg/v3/console" 6 | "github.com/minio/warp/pkg/bench" 7 | ) 8 | 9 | var multipartPutFlags = []cli.Flag{ 10 | cli.IntFlag{ 11 | Name: "parts", 12 | Value: 100, 13 | Usage: "Number of parts to upload for each multipart upload", 14 | }, 15 | cli.StringFlag{ 16 | Name: "part.size", 17 | Value: "5MiB", 18 | Usage: "Size of each part. Can be a number or MiB/GiB.", 19 | }, 20 | cli.IntFlag{ 21 | Name: "part.concurrent", 22 | Value: 20, 23 | Usage: "Run this many concurrent operations per each multipart upload. Must not exceed a number of parts.", 24 | }, 25 | } 26 | 27 | var MultiPartPutCombinedFlags = combineFlags(globalFlags, ioFlags, multipartPutFlags, genFlags, benchFlags, analyzeFlags) 28 | 29 | // MultipartPut command 30 | var multipartPutCmd = cli.Command{ 31 | Name: "multipart-put", 32 | Usage: "benchmark multipart upload", 33 | Action: mainMutipartPut, 34 | Before: setGlobalsFromContext, 35 | Flags: MultiPartPutCombinedFlags, 36 | CustomHelpTemplate: `NAME: 37 | {{.HelpName}} - {{.Usage}} 38 | 39 | USAGE: 40 | {{.HelpName}} [FLAGS] 41 | -> see https://github.com/minio/warp#multipart-put 42 | 43 | FLAGS: 44 | {{range .VisibleFlags}}{{.}} 45 | {{end}}`, 46 | } 47 | 48 | // mainMutipartPut is the entry point for multipart-put command 49 | func mainMutipartPut(ctx *cli.Context) error { 50 | checkMultipartPutSyntax(ctx) 51 | 52 | b := &bench.MultipartPut{ 53 | Common: getCommon(ctx, newGenSource(ctx, "part.size")), 54 | PartsNumber: ctx.Int("parts"), 55 | PartsConcurrency: ctx.Int("part.concurrent"), 56 | } 57 | return runBench(ctx, b) 58 | } 59 | 60 | func checkMultipartPutSyntax(ctx *cli.Context) { 61 | if ctx.NArg() > 0 { 62 | console.Fatal("Command takes no arguments") 63 | } 64 | if ctx.Bool("disable-multipart") { 65 | console.Fatal("cannot disable multipart for multipart-put test") 66 | } 67 | 68 | if ctx.Int("parts") > 10000 { 69 | console.Fatal("parts can't be more than 10000") 70 | } 71 | if ctx.Int("parts") <= 0 { 72 | console.Fatal("parts must be at least 1") 73 | } 74 | 75 | if ctx.Int("part.concurrent") > ctx.Int("parts") { 76 | console.Fatal("part.concurrent can't be more than parts") 77 | } 78 | 79 | sz, err := toSize(ctx.String("part.size")) 80 | if err != nil { 81 | console.Fatal("error parsing part.size:", err) 82 | } 83 | if sz <= 0 { 84 | console.Fatal("part.size must be at least 1") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cli/print.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "encoding/json" 22 | "fmt" 23 | "strings" 24 | "sync" 25 | "unicode" 26 | 27 | "github.com/cheggaaa/pb" 28 | "github.com/minio/mc/pkg/probe" 29 | "github.com/minio/pkg/v3/console" 30 | ) 31 | 32 | // causeMessage container for golang error messages 33 | type causeMessage struct { 34 | Error error `json:"error"` 35 | Message string `json:"message"` 36 | } 37 | 38 | // errorMessage container for error messages 39 | type errorMessage struct { 40 | Cause causeMessage `json:"cause"` 41 | SysInfo map[string]string `json:"sysinfo"` 42 | Message string `json:"message"` 43 | Type string `json:"type"` 44 | CallTrace []probe.TracePoint `json:"trace,omitempty"` 45 | } 46 | 47 | var printMu sync.Mutex 48 | 49 | func printError(data ...interface{}) { 50 | printMu.Lock() 51 | defer printMu.Unlock() 52 | w, _ := pb.GetTerminalWidth() 53 | if w > 0 { 54 | fmt.Print("\r", strings.Repeat(" ", w), "\r") 55 | } else { 56 | data = append(data, "\n") 57 | } 58 | console.Errorln(data...) 59 | } 60 | 61 | // fatalIf wrapper function which takes error and selectively prints stack frames if available on debug 62 | func fatalIf(err *probe.Error, msg string, data ...interface{}) { 63 | if err == nil { 64 | return 65 | } 66 | fatal(err, msg, data...) 67 | } 68 | 69 | func fatal(err *probe.Error, msg string, data ...interface{}) { 70 | if globalJSON { 71 | errorMsg := errorMessage{ 72 | Message: msg, 73 | Type: "fatal", 74 | Cause: causeMessage{ 75 | Message: err.ToGoError().Error(), 76 | Error: err.ToGoError(), 77 | }, 78 | SysInfo: err.SysInfo, 79 | } 80 | if globalDebug { 81 | errorMsg.CallTrace = err.CallTrace 82 | } 83 | json, e := json.MarshalIndent(struct { 84 | Error errorMessage `json:"error"` 85 | Status string `json:"status"` 86 | }{ 87 | Status: "error", 88 | Error: errorMsg, 89 | }, "", " ") 90 | if e != nil { 91 | console.Fatalln(probe.NewError(e)) 92 | } 93 | console.Infoln(string(json)) 94 | console.Fatalln() 95 | } 96 | 97 | msg = fmt.Sprintf(msg, data...) 98 | errmsg := err.String() 99 | if !globalDebug { 100 | errmsg = err.ToGoError().Error() 101 | } 102 | 103 | // Remove unnecessary leading spaces in generic/detailed error messages 104 | msg = strings.TrimSpace(msg) 105 | errmsg = strings.TrimSpace(errmsg) 106 | 107 | // Add punctuations when needed 108 | if len(errmsg) > 0 && len(msg) > 0 { 109 | if msg[len(msg)-1] != ':' && msg[len(msg)-1] != '.' { 110 | // The detailed error message starts with a capital letter, 111 | // we should then add '.', otherwise add ':'. 112 | if unicode.IsUpper(rune(errmsg[0])) { 113 | msg += "." 114 | } else { 115 | msg += ":" 116 | } 117 | } 118 | // Add '.' to the detail error if not found 119 | if errmsg[len(errmsg)-1] != '.' { 120 | errmsg += "." 121 | } 122 | } 123 | fmt.Println("") 124 | console.Fatalln(fmt.Sprintf("%s %s", msg, errmsg)) 125 | } 126 | 127 | // errorIf synonymous with fatalIf but doesn't exit on error != nil 128 | func errorIf(err *probe.Error, msg string, data ...interface{}) { 129 | if err == nil { 130 | return 131 | } 132 | if globalJSON { 133 | errorMsg := errorMessage{ 134 | Message: fmt.Sprintf(msg, data...), 135 | Type: "error", 136 | Cause: causeMessage{ 137 | Message: err.ToGoError().Error(), 138 | Error: err.ToGoError(), 139 | }, 140 | SysInfo: err.SysInfo, 141 | } 142 | if globalDebug { 143 | errorMsg.CallTrace = err.CallTrace 144 | } 145 | json, e := json.MarshalIndent(struct { 146 | Error errorMessage `json:"error"` 147 | Status string `json:"status"` 148 | }{ 149 | Status: "error", 150 | Error: errorMsg, 151 | }, "", " ") 152 | if e != nil { 153 | console.Fatalln(probe.NewError(e)) 154 | } 155 | console.Infoln(string(json)) 156 | return 157 | } 158 | msg = fmt.Sprintf(msg, data...) 159 | if !globalDebug { 160 | console.Errorln(fmt.Sprintf("%s %s", msg, err.ToGoError())) 161 | return 162 | } 163 | fmt.Println("") 164 | console.Errorln(fmt.Sprintf("%s %s", msg, err)) 165 | } 166 | -------------------------------------------------------------------------------- /cli/put.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "fmt" 22 | "math/rand" 23 | "strings" 24 | 25 | "github.com/minio/cli" 26 | "github.com/minio/minio-go/v7" 27 | "github.com/minio/pkg/v3/console" 28 | "github.com/minio/warp/pkg/bench" 29 | ) 30 | 31 | var putFlags = []cli.Flag{ 32 | cli.StringFlag{ 33 | Name: "obj.size", 34 | Value: "10MiB", 35 | Usage: "Size of each generated object. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 36 | }, 37 | cli.StringFlag{ 38 | Name: "part.size", 39 | Value: "", 40 | Usage: "Multipart part size. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 41 | Hidden: true, 42 | }, 43 | cli.BoolFlag{ 44 | Name: "post", 45 | Usage: "Use PostObject for upload. Will force single part upload", 46 | }, 47 | } 48 | 49 | var PutCombinedFlags = combineFlags(globalFlags, ioFlags, putFlags, genFlags, benchFlags, analyzeFlags) 50 | 51 | // Put command. 52 | var putCmd = cli.Command{ 53 | Name: "put", 54 | Usage: "benchmark put objects", 55 | Action: mainPut, 56 | Before: setGlobalsFromContext, 57 | Flags: PutCombinedFlags, 58 | CustomHelpTemplate: `NAME: 59 | {{.HelpName}} - {{.Usage}} 60 | 61 | USAGE: 62 | {{.HelpName}} [FLAGS] 63 | -> see https://github.com/minio/warp#put 64 | 65 | FLAGS: 66 | {{range .VisibleFlags}}{{.}} 67 | {{end}}`, 68 | } 69 | 70 | // mainPut is the entry point for cp command. 71 | func mainPut(ctx *cli.Context) error { 72 | checkPutSyntax(ctx) 73 | b := bench.Put{ 74 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 75 | PostObject: ctx.Bool("post"), 76 | } 77 | return runBench(ctx, &b) 78 | } 79 | 80 | const metadataChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_." 81 | 82 | // putOpts retrieves put options from the context. 83 | func putOpts(ctx *cli.Context) minio.PutObjectOptions { 84 | pSize, _ := toSize(ctx.String("part.size")) 85 | options := minio.PutObjectOptions{ 86 | ServerSideEncryption: newSSE(ctx), 87 | DisableMultipart: ctx.Bool("disable-multipart"), 88 | DisableContentSha256: ctx.Bool("disable-sha256-payload"), 89 | SendContentMd5: ctx.Bool("md5"), 90 | StorageClass: ctx.String("storage-class"), 91 | PartSize: pSize, 92 | } 93 | 94 | for _, flag := range []string{"add-metadata", "tag"} { 95 | values := make(map[string]string) 96 | 97 | for _, v := range ctx.StringSlice(flag) { 98 | idx := strings.Index(v, "=") 99 | if idx <= 0 { 100 | console.Fatalf("--%s takes `key=value` argument", flag) 101 | } 102 | key := v[:idx] 103 | value := v[idx+1:] 104 | if len(value) == 0 { 105 | console.Fatal("--%s value can't be empty", flag) 106 | } 107 | var randN int 108 | if _, err := fmt.Sscanf(value, "rand:%d", &randN); err == nil { 109 | rng := rand.New(rand.NewSource(int64(rand.Uint64()))) 110 | value = "" 111 | for i := 0; i < randN; i++ { 112 | value += string(metadataChars[rng.Int()%len(metadataChars)]) 113 | } 114 | } 115 | values[key] = value 116 | } 117 | 118 | switch flag { 119 | case "metadata": 120 | options.UserMetadata = values 121 | case "tag": 122 | options.UserTags = values 123 | } 124 | } 125 | 126 | return options 127 | } 128 | 129 | func checkPutSyntax(ctx *cli.Context) { 130 | if ctx.NArg() > 0 { 131 | console.Fatal("Command takes no arguments") 132 | } 133 | 134 | checkAnalyze(ctx) 135 | checkBenchmark(ctx) 136 | } 137 | -------------------------------------------------------------------------------- /cli/retention.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "github.com/minio/cli" 22 | "github.com/minio/pkg/v3/console" 23 | "github.com/minio/warp/pkg/bench" 24 | ) 25 | 26 | var retentionFlags = []cli.Flag{ 27 | cli.IntFlag{ 28 | Name: "objects", 29 | Value: 25000, 30 | Usage: "Number of objects to upload.", 31 | }, 32 | cli.IntFlag{ 33 | Name: "versions", 34 | Value: 5, 35 | Usage: "Number of versions to upload to each object", 36 | }, 37 | cli.StringFlag{ 38 | Name: "obj.size", 39 | Value: "1KiB", 40 | Usage: "Size of each generated object. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 41 | }, 42 | } 43 | 44 | var RetentionCombinedFlags = combineFlags(globalFlags, ioFlags, retentionFlags, genFlags, benchFlags, analyzeFlags) 45 | 46 | var retentionCmd = cli.Command{ 47 | Name: "retention", 48 | Usage: "benchmark PutObjectRetention", 49 | Action: mainRetention, 50 | Before: setGlobalsFromContext, 51 | Flags: RetentionCombinedFlags, 52 | CustomHelpTemplate: `NAME: 53 | {{.HelpName}} - {{.Usage}} 54 | 55 | USAGE: 56 | {{.HelpName}} [FLAGS] 57 | -> see https://github.com/minio/warp#retention 58 | 59 | FLAGS: 60 | {{range .VisibleFlags}}{{.}} 61 | {{end}}`, 62 | } 63 | 64 | // mainGet is the entry point for get command. 65 | func mainRetention(ctx *cli.Context) error { 66 | checkRetentionSyntax(ctx) 67 | b := bench.Retention{ 68 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 69 | CreateObjects: ctx.Int("objects"), 70 | Versions: ctx.Int("versions"), 71 | } 72 | b.Locking = true 73 | return runBench(ctx, &b) 74 | } 75 | 76 | func checkRetentionSyntax(ctx *cli.Context) { 77 | if ctx.NArg() > 0 { 78 | console.Fatal("Command takes no arguments") 79 | } 80 | if ctx.Int("objects") <= 0 { 81 | console.Fatal("There must be more than 0 objects.") 82 | } 83 | if ctx.Int("versions") <= 0 { 84 | console.Fatal("There must be more than 0 versions per object.") 85 | } 86 | 87 | checkAnalyze(ctx) 88 | checkBenchmark(ctx) 89 | } 90 | -------------------------------------------------------------------------------- /cli/rlimit.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "runtime/debug" 22 | 23 | "github.com/minio/pkg/v3/sys" 24 | ) 25 | 26 | func setMaxResources() (err error) { 27 | // Set the Go runtime max threads threshold to 90% of kernel setting. 28 | // Do not return when an error when encountered since it is not a crucial task. 29 | sysMaxThreads, mErr := sys.GetMaxThreads() 30 | if mErr == nil { 31 | minioMaxThreads := (sysMaxThreads * 90) / 100 32 | // Only set max threads if it is greater than the default one 33 | if minioMaxThreads > 10000 { 34 | debug.SetMaxThreads(minioMaxThreads) 35 | } 36 | } 37 | 38 | var maxLimit uint64 39 | 40 | // Set open files limit to maximum. 41 | if _, maxLimit, err = sys.GetMaxOpenFileLimit(); err != nil { 42 | return err 43 | } 44 | 45 | if err = sys.SetMaxOpenFileLimit(maxLimit, maxLimit); err != nil { 46 | return err 47 | } 48 | 49 | // Set max memory limit as current memory limit. 50 | if _, maxLimit, err = sys.GetMaxMemoryLimit(); err != nil { 51 | return err 52 | } 53 | 54 | err = sys.SetMaxMemoryLimit(maxLimit, maxLimit) 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /cli/snowball.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2023 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "github.com/minio/cli" 22 | "github.com/minio/minio-go/v7" 23 | "github.com/minio/pkg/v3/console" 24 | "github.com/minio/warp/pkg/bench" 25 | ) 26 | 27 | var snowballFlags = []cli.Flag{ 28 | cli.StringFlag{ 29 | Name: "obj.size", 30 | Value: "512KiB", 31 | Usage: "Size of each generated object inside the snowball. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 32 | }, 33 | cli.IntFlag{ 34 | Name: "objs.per", 35 | Value: 50, 36 | Usage: "Number of objects per snowball upload.", 37 | }, 38 | cli.BoolFlag{ 39 | Name: "compress", 40 | Usage: "Compress each snowball file. Available for MinIO servers only.", 41 | }, 42 | } 43 | 44 | // Put command. 45 | var snowballCmd = cli.Command{ 46 | Name: "snowball", 47 | Usage: "benchmark put objects in snowball tar files", 48 | Action: mainSnowball, 49 | Before: setGlobalsFromContext, 50 | Flags: combineFlags(globalFlags, ioFlags, snowballFlags, genFlags, benchFlags, analyzeFlags), 51 | CustomHelpTemplate: `NAME: 52 | {{.HelpName}} - {{.Usage}} 53 | 54 | USAGE: 55 | {{.HelpName}} [FLAGS] 56 | -> see https://github.com/minio/warp#snowball 57 | 58 | FLAGS: 59 | {{range .VisibleFlags}}{{.}} 60 | {{end}}`, 61 | } 62 | 63 | // mainPut is the entry point for cp command. 64 | func mainSnowball(ctx *cli.Context) error { 65 | checkSnowballSyntax(ctx) 66 | b := bench.Snowball{ 67 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 68 | Compress: ctx.Bool("compress"), 69 | Duplicate: ctx.Bool("compress"), 70 | NumObjs: ctx.Int("objs.per"), 71 | } 72 | b.PutOpts = snowballOpts(ctx) 73 | if b.Compress { 74 | sz, err := toSize(ctx.String("obj.size")) 75 | if err != nil { 76 | return err 77 | } 78 | b.WindowSize = int(sz) * 2 79 | if b.WindowSize < 128<<10 { 80 | b.WindowSize = 128 << 10 81 | } 82 | if b.WindowSize > 16<<20 { 83 | b.WindowSize = 16 << 20 84 | } 85 | } 86 | return runBench(ctx, &b) 87 | } 88 | 89 | // putOpts retrieves put options from the context. 90 | func snowballOpts(ctx *cli.Context) minio.PutObjectOptions { 91 | return minio.PutObjectOptions{ 92 | ServerSideEncryption: newSSE(ctx), 93 | DisableMultipart: ctx.Bool("disable-multipart"), 94 | DisableContentSha256: ctx.Bool("disable-sha256-payload"), 95 | SendContentMd5: ctx.Bool("md5"), 96 | StorageClass: ctx.String("storage-class"), 97 | } 98 | } 99 | 100 | func checkSnowballSyntax(ctx *cli.Context) { 101 | if ctx.NArg() > 0 { 102 | console.Fatal("Command takes no arguments") 103 | } 104 | 105 | // 1GB max total. 106 | const maxSize = 1 << 30 107 | sz, err := toSize(ctx.String("obj.size")) 108 | if err != nil { 109 | console.Fatalf("Unable to parse --obj.size: %v", err) 110 | } 111 | gotTotal := int64(sz) * int64(ctx.Int("concurrent")) 112 | compress := ctx.Bool("compress") 113 | if compress && sz > 10<<20 { 114 | console.Fatal("--obj.size must be <= 10MiB when compression is enabled") 115 | } 116 | if !compress { 117 | gotTotal *= int64(ctx.Int("objs.per")) 118 | } 119 | if gotTotal > maxSize { 120 | console.Fatalf("total size (%d) exceeds 1GiB", gotTotal) 121 | } 122 | if gotTotal <= 0 { 123 | console.Fatalf("Parameters results in no expected output") 124 | } 125 | checkAnalyze(ctx) 126 | checkBenchmark(ctx) 127 | } 128 | -------------------------------------------------------------------------------- /cli/sse.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "crypto/rand" 22 | 23 | "github.com/minio/cli" 24 | "github.com/minio/minio-go/v7/pkg/encrypt" 25 | ) 26 | 27 | var sseKey encrypt.ServerSide 28 | 29 | // newSSE returns a randomly generated key if SSE is requested. 30 | // Only one key will be generated. 31 | func newSSE(ctx *cli.Context) encrypt.ServerSide { 32 | if !ctx.Bool("encrypt") && !ctx.Bool("sse-s3-encrypt") { 33 | return nil 34 | } 35 | if sseKey != nil { 36 | return sseKey 37 | } 38 | 39 | if ctx.Bool("sse-s3-encrypt") { 40 | sseKey = encrypt.NewSSE() 41 | return sseKey 42 | } 43 | 44 | var key [32]byte 45 | _, err := rand.Read(key[:]) 46 | if err != nil { 47 | panic(err) 48 | } 49 | sseKey, err = encrypt.NewSSEC(key[:]) 50 | if err != nil { 51 | panic(err) 52 | } 53 | return sseKey 54 | } 55 | -------------------------------------------------------------------------------- /cli/stat.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "github.com/minio/cli" 22 | "github.com/minio/minio-go/v7" 23 | "github.com/minio/pkg/v3/console" 24 | "github.com/minio/warp/pkg/bench" 25 | ) 26 | 27 | var statFlags = []cli.Flag{ 28 | cli.IntFlag{ 29 | Name: "objects", 30 | Value: 10000, 31 | Usage: "Number of objects to upload. Rounded to have equal concurrent objects.", 32 | }, 33 | cli.StringFlag{ 34 | Name: "obj.size", 35 | Value: "1KB", 36 | Usage: "Size of each generated object. Can be a number or 10KB/MB/GB. All sizes are base 2 binary.", 37 | }, 38 | cli.IntFlag{ 39 | Name: "versions", 40 | Value: 1, 41 | Usage: "Number of versions to upload. If more than 1, versioned listing will be benchmarked", 42 | }, 43 | cli.BoolFlag{ 44 | Name: "list-existing", 45 | Usage: "Instead of preparing the bench by PUTing some objects, only use objects already in the bucket", 46 | }, 47 | cli.BoolFlag{ 48 | Name: "list-flat", 49 | Usage: "When using --list-existing, do not use recursive listing", 50 | }, 51 | } 52 | 53 | var StatCombinedFlags = combineFlags(globalFlags, ioFlags, statFlags, genFlags, benchFlags, analyzeFlags) 54 | 55 | var statCmd = cli.Command{ 56 | Name: "stat", 57 | Usage: "benchmark stat objects (get file info)", 58 | Action: mainStat, 59 | Before: setGlobalsFromContext, 60 | Flags: StatCombinedFlags, 61 | CustomHelpTemplate: `NAME: 62 | {{.HelpName}} - {{.Usage}} 63 | 64 | USAGE: 65 | {{.HelpName}} [FLAGS] 66 | -> see https://github.com/minio/warp#stat 67 | 68 | FLAGS: 69 | {{range .VisibleFlags}}{{.}} 70 | {{end}}`, 71 | } 72 | 73 | // mainDelete is the entry point for get command. 74 | func mainStat(ctx *cli.Context) error { 75 | checkStatSyntax(ctx) 76 | sse := newSSE(ctx) 77 | 78 | b := bench.Stat{ 79 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 80 | Versions: ctx.Int("versions"), 81 | CreateObjects: ctx.Int("objects"), 82 | StatOpts: minio.StatObjectOptions{ 83 | ServerSideEncryption: sse, 84 | }, 85 | ListExisting: ctx.Bool("list-existing"), 86 | ListFlat: ctx.Bool("list-flat"), 87 | ListPrefix: ctx.String("prefix"), 88 | } 89 | return runBench(ctx, &b) 90 | } 91 | 92 | func checkStatSyntax(ctx *cli.Context) { 93 | if ctx.NArg() > 0 { 94 | console.Fatal("Command takes no arguments") 95 | } 96 | if ctx.Int("versions") < 1 { 97 | console.Fatal("At least one version must be tested") 98 | } 99 | if ctx.Int("objects") < 1 { 100 | console.Fatal("At least one object must be tested") 101 | } 102 | checkAnalyze(ctx) 103 | checkBenchmark(ctx) 104 | } 105 | -------------------------------------------------------------------------------- /cli/versioned.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "net/http" 22 | 23 | "github.com/minio/cli" 24 | "github.com/minio/mc/pkg/probe" 25 | "github.com/minio/minio-go/v7" 26 | "github.com/minio/pkg/v3/console" 27 | "github.com/minio/warp/pkg/bench" 28 | ) 29 | 30 | var versionedFlags = []cli.Flag{ 31 | cli.IntFlag{ 32 | Name: "objects", 33 | Value: 250, 34 | Usage: "Number of objects to upload.", 35 | }, 36 | cli.StringFlag{ 37 | Name: "obj.size", 38 | Value: "10MiB", 39 | Usage: "Size of each generated object. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 40 | }, 41 | cli.Float64Flag{ 42 | Name: "get-distrib", 43 | Usage: "The amount of GET operations.", 44 | Value: 45, 45 | }, 46 | cli.Float64Flag{ 47 | Name: "stat-distrib", 48 | Usage: "The amount of STAT operations.", 49 | Value: 30, 50 | }, 51 | cli.Float64Flag{ 52 | Name: "put-distrib", 53 | Usage: "The amount of PUT operations.", 54 | Value: 15, 55 | }, 56 | cli.Float64Flag{ 57 | Name: "delete-distrib", 58 | Usage: "The amount of DELETE operations. Must be at least the same as PUT.", 59 | Value: 10, 60 | }, 61 | } 62 | 63 | var VersionedCombinedFlags = combineFlags(globalFlags, ioFlags, versionedFlags, genFlags, benchFlags, analyzeFlags) 64 | 65 | var versionedCmd = cli.Command{ 66 | Name: "versioned", 67 | Usage: "benchmark mixed versioned objects", 68 | Action: mainVersioned, 69 | Before: setGlobalsFromContext, 70 | Flags: VersionedCombinedFlags, 71 | CustomHelpTemplate: `NAME: 72 | {{.HelpName}} - {{.Usage}} 73 | 74 | USAGE: 75 | {{.HelpName}} [FLAGS] 76 | -> see https://github.com/minio/warp#mixed 77 | 78 | FLAGS: 79 | {{range .VisibleFlags}}{{.}} 80 | {{end}}`, 81 | } 82 | 83 | // mainVersioned is the entry point for mixed command. 84 | func mainVersioned(ctx *cli.Context) error { 85 | checkVersionedSyntax(ctx) 86 | sse := newSSE(ctx) 87 | dist := bench.VersionedDistribution{ 88 | Distribution: map[string]float64{ 89 | http.MethodGet: ctx.Float64("get-distrib"), 90 | "STAT": ctx.Float64("stat-distrib"), 91 | http.MethodPut: ctx.Float64("put-distrib"), 92 | http.MethodDelete: ctx.Float64("delete-distrib"), 93 | }, 94 | } 95 | err := dist.Generate(ctx.Int("objects") * 2) 96 | fatalIf(probe.NewError(err), "Invalid distribution") 97 | b := bench.Versioned{ 98 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 99 | CreateObjects: ctx.Int("objects"), 100 | GetOpts: minio.GetObjectOptions{ServerSideEncryption: sse}, 101 | StatOpts: minio.StatObjectOptions{ 102 | ServerSideEncryption: sse, 103 | }, 104 | Dist: &dist, 105 | } 106 | return runBench(ctx, &b) 107 | } 108 | 109 | func checkVersionedSyntax(ctx *cli.Context) { 110 | if ctx.NArg() > 0 { 111 | console.Fatal("Command takes no arguments") 112 | } 113 | if ctx.Int("objects") < 1 { 114 | console.Fatal("At least one object must be tested") 115 | } 116 | checkAnalyze(ctx) 117 | checkBenchmark(ctx) 118 | } 119 | -------------------------------------------------------------------------------- /cli/zip.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2022 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package cli 19 | 20 | import ( 21 | "fmt" 22 | "time" 23 | 24 | "github.com/minio/cli" 25 | "github.com/minio/pkg/v3/console" 26 | "github.com/minio/warp/pkg/bench" 27 | ) 28 | 29 | var zipFlags = []cli.Flag{ 30 | cli.IntFlag{ 31 | Name: "files", 32 | Value: 10000, 33 | Usage: "Number of files to upload in the zip file.", 34 | }, 35 | cli.StringFlag{ 36 | Name: "obj.size", 37 | Value: "10KiB", 38 | Usage: "Size of each generated object. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 39 | }, 40 | cli.StringFlag{ 41 | Name: "part.size", 42 | Value: "", 43 | Usage: "Multipart part size. Can be a number or 10KiB/MiB/GiB. All sizes are base 2 binary.", 44 | Hidden: true, 45 | }, 46 | } 47 | 48 | var ZipCombinedFlags = combineFlags(globalFlags, ioFlags, zipFlags, genFlags, benchFlags, analyzeFlags) 49 | 50 | var zipCmd = cli.Command{ 51 | Name: "zip", 52 | Usage: "benchmark minio s3zip", 53 | Action: mainZip, 54 | Before: setGlobalsFromContext, 55 | Flags: ZipCombinedFlags, 56 | CustomHelpTemplate: `NAME: 57 | {{.HelpName}} - {{.Usage}} 58 | 59 | USAGE: 60 | {{.HelpName}} [FLAGS] 61 | -> see https://github.com/minio/warp#zip 62 | 63 | FLAGS: 64 | {{range .VisibleFlags}}{{.}} 65 | {{end}}`, 66 | } 67 | 68 | // mainGet is the entry point for get command. 69 | func mainZip(ctx *cli.Context) error { 70 | checkZipSyntax(ctx) 71 | ctx.Set("noprefix", "true") 72 | b := bench.S3Zip{ 73 | Common: getCommon(ctx, newGenSource(ctx, "obj.size")), 74 | CreateFiles: ctx.Int("files"), 75 | ZipObjName: fmt.Sprintf("%d.zip", time.Now().UnixNano()), 76 | } 77 | b.Locking = true 78 | return runBench(ctx, &b) 79 | } 80 | 81 | func checkZipSyntax(ctx *cli.Context) { 82 | if ctx.NArg() > 0 { 83 | console.Fatal("Command takes no arguments") 84 | } 85 | if ctx.Int("files") <= 0 { 86 | console.Fatal("There must be more than 0 objects.") 87 | } 88 | 89 | checkAnalyze(ctx) 90 | checkBenchmark(ctx) 91 | } 92 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/minio/warp 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/bygui86/multi-profile/v2 v2.1.0 9 | github.com/charmbracelet/bubbles v0.21.0 10 | github.com/charmbracelet/bubbletea v1.3.4 11 | github.com/charmbracelet/lipgloss v1.1.0 12 | github.com/cheggaaa/pb v1.0.29 13 | github.com/dustin/go-humanize v1.0.1 14 | github.com/fatih/color v1.18.0 15 | github.com/influxdata/influxdb-client-go/v2 v2.14.0 16 | github.com/jfsmig/prng v0.0.2 17 | github.com/klauspost/compress v1.18.0 18 | github.com/minio/cli v1.24.2 19 | github.com/minio/madmin-go/v4 v4.0.13 20 | github.com/minio/mc v0.0.0-20250506164133-19d87ba47505 21 | github.com/minio/md5-simd v1.1.2 22 | github.com/minio/minio-go/v7 v7.0.92-0.20250515110726-4f25bfc12706 23 | github.com/minio/pkg/v3 v3.1.8 24 | github.com/minio/websocket v1.6.0 25 | github.com/muesli/termenv v0.16.0 26 | github.com/posener/complete v1.2.3 27 | golang.org/x/net v0.39.0 28 | golang.org/x/sync v0.13.0 29 | golang.org/x/time v0.11.0 30 | gopkg.in/yaml.v3 v3.0.1 31 | ) 32 | 33 | require ( 34 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect 35 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 36 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 37 | github.com/charmbracelet/colorprofile v0.3.1 // indirect 38 | github.com/charmbracelet/harmonica v0.2.0 // indirect 39 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 40 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 41 | github.com/charmbracelet/x/term v0.2.1 // indirect 42 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 43 | github.com/ebitengine/purego v0.8.3 // indirect 44 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 45 | github.com/go-ini/ini v1.67.0 // indirect 46 | github.com/go-ole/go-ole v1.3.0 // indirect 47 | github.com/goccy/go-json v0.10.5 // indirect 48 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 49 | github.com/golang/protobuf v1.5.4 // indirect 50 | github.com/google/uuid v1.6.0 // indirect 51 | github.com/hashicorp/errwrap v1.1.0 // indirect 52 | github.com/hashicorp/go-multierror v1.1.1 // indirect 53 | github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect 54 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 55 | github.com/lestrrat-go/blackmagic v1.0.3 // indirect 56 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 57 | github.com/lestrrat-go/httprc v1.0.6 // indirect 58 | github.com/lestrrat-go/iter v1.0.2 // indirect 59 | github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect 60 | github.com/lestrrat-go/option v1.0.1 // indirect 61 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 62 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect 63 | github.com/mattn/go-colorable v0.1.14 // indirect 64 | github.com/mattn/go-isatty v0.0.20 // indirect 65 | github.com/mattn/go-localereader v0.0.1 // indirect 66 | github.com/mattn/go-runewidth v0.0.16 // indirect 67 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 68 | github.com/minio/crc64nvme v1.0.1 // indirect 69 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 70 | github.com/muesli/cancelreader v0.2.2 // indirect 71 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 72 | github.com/oapi-codegen/runtime v1.1.1 // indirect 73 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect 74 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 75 | github.com/prometheus/client_model v0.6.2 // indirect 76 | github.com/prometheus/common v0.63.0 // indirect 77 | github.com/prometheus/procfs v0.16.1 // indirect 78 | github.com/prometheus/prom2json v1.4.2 // indirect 79 | github.com/prometheus/prometheus v0.303.1 // indirect 80 | github.com/rivo/uniseg v0.4.7 // indirect 81 | github.com/rjeczalik/notify v0.9.3 // indirect 82 | github.com/rs/xid v1.6.0 // indirect 83 | github.com/safchain/ethtool v0.5.10 // indirect 84 | github.com/secure-io/sio-go v0.3.1 // indirect 85 | github.com/segmentio/asm v1.2.0 // indirect 86 | github.com/shirou/gopsutil/v4 v4.25.4 // indirect 87 | github.com/tinylib/msgp v1.3.0 // indirect 88 | github.com/tklauser/go-sysconf v0.3.15 // indirect 89 | github.com/tklauser/numcpus v0.10.0 // indirect 90 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 91 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 92 | golang.org/x/crypto v0.37.0 // indirect 93 | golang.org/x/sys v0.32.0 // indirect 94 | golang.org/x/text v0.24.0 // indirect 95 | google.golang.org/protobuf v1.36.6 // indirect 96 | ) 97 | -------------------------------------------------------------------------------- /k8s/README.md: -------------------------------------------------------------------------------- 1 | # Running *warp* on kubernetes 2 | 3 | This document describes with simple examples on how to automate running *warp* on Kubernetes with `yaml` files. You can also use [Warp Helm Chart](./helm) to 4 | deploy Warp. For details on Helm chart based deployment, refer the [document here](./helm/README.md). 5 | 6 | ## Create *warp* client listeners 7 | 8 | Create *warp* client listeners to run distributed *warp* benchmark, here we will run them as stateful sets across client nodes. 9 | 10 | ``` 11 | ~ kubectl create -f warp.yaml 12 | ``` 13 | 14 | ``` 15 | ~ kubectl get pods -l app=warp 16 | NAME READY STATUS RESTARTS AGE 17 | warp-0 1/1 Running 0 6m53s 18 | warp-1 1/1 Running 0 6m57s 19 | warp-2 1/1 Running 0 7m8s 20 | warp-3 1/1 Running 0 7m17s 21 | ``` 22 | 23 | Now prepare your *warp-job.yaml* (we have included a sample please edit for your needs) to benchmark your MinIO cluster 24 | ``` 25 | ~ kubectl create -f warp-job.yaml 26 | ``` 27 | 28 | ``` 29 | ~ kubectl get pods -l job-name=warp-job 30 | NAME READY STATUS RESTARTS AGE 31 | warp-job-6xt5k 0/1 Completed 0 8m53s 32 | ``` 33 | 34 | To obtain the console output look at the job logs 35 | ``` 36 | ~ kubectl logs warp-job-6xt5k 37 | ... 38 | ... 39 | ------------------- 40 | Operation: PUT. Concurrency: 256. Hosts: 4. 41 | * Average: 412.73 MiB/s, 12.90 obj/s (1m48.853s, starting 19:14:51 UTC) 42 | 43 | Throughput by host: 44 | * http://minio-0.minio.default.svc.cluster.local:9000: Avg: 101.52 MiB/s, 3.17 obj/s (2m32.632s, starting 19:14:30 UTC) 45 | * http://minio-1.minio.default.svc.cluster.local:9000: Avg: 103.82 MiB/s, 3.24 obj/s (2m32.654s, starting 19:14:30 UTC) 46 | * http://minio-2.minio.default.svc.cluster.local:9000: Avg: 103.39 MiB/s, 3.23 obj/s (2m32.635s, starting 19:14:30 UTC) 47 | * http://minio-3.minio.default.svc.cluster.local:9000: Avg: 105.31 MiB/s, 3.29 obj/s (2m32.636s, starting 19:14:30 UTC) 48 | 49 | Aggregated Throughput, split into 108 x 1s time segments: 50 | * Fastest: 677.1MiB/s, 21.16 obj/s (1s, starting 19:15:54 UTC) 51 | * 50% Median: 406.4MiB/s, 12.70 obj/s (1s, starting 19:14:51 UTC) 52 | * Slowest: 371.5MiB/s, 11.61 obj/s (1s, starting 19:15:42 UTC) 53 | ------------------- 54 | Operation: GET. Concurrency: 256. Hosts: 4. 55 | * Average: 866.56 MiB/s, 27.08 obj/s (4m28.204s, starting 19:17:30 UTC) 56 | 57 | Throughput by host: 58 | * http://minio-0.minio.default.svc.cluster.local:9000: Avg: 180.39 MiB/s, 5.64 obj/s (4m59.817s, starting 19:17:12 UTC) 59 | * http://minio-1.minio.default.svc.cluster.local:9000: Avg: 179.02 MiB/s, 5.59 obj/s (4m59.24s, starting 19:17:12 UTC) 60 | * http://minio-2.minio.default.svc.cluster.local:9000: Avg: 182.98 MiB/s, 5.72 obj/s (4m59.697s, starting 19:17:12 UTC) 61 | * http://minio-3.minio.default.svc.cluster.local:9000: Avg: 322.47 MiB/s, 10.08 obj/s (4m59.929s, starting 19:17:12 UTC) 62 | 63 | Aggregated Throughput, split into 268 x 1s time segments: 64 | * Fastest: 881.9MiB/s, 27.56 obj/s (1s, starting 19:20:49 UTC) 65 | * 50% Median: 866.4MiB/s, 27.08 obj/s (1s, starting 19:18:46 UTC) 66 | * Slowest: 851.4MiB/s, 26.61 obj/s (1s, starting 19:17:37 UTC) 67 | ``` 68 | -------------------------------------------------------------------------------- /k8s/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: warp 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | version: 0.1.0 18 | 19 | # This is the version number of the application being deployed. This version number should be 20 | # incremented each time you make changes to the application. 21 | appVersion: latest 22 | -------------------------------------------------------------------------------- /k8s/helm/README.md: -------------------------------------------------------------------------------- 1 | # Warp Helm Chart 2 | 3 | ### Introduction 4 | 5 | This chart bootstraps Warp deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. 6 | 7 | ### Prerequisites 8 | 9 | - Kubernetes 1.5+. 10 | - Clone this repository in a local path, for example `/home/warp`. 11 | 12 | ### Configuring the Chart 13 | 14 | The [configuration](./values.yaml) file lists the configuration parameters. If you cloned the repo to `/home/warp`, edit the `/home/warp/k8s/helm/values.yaml` file to configure MinIO Server endpoint, credentials and other relevant fields explained in the [Warp documentation](https://github.com/minio/warp#usage). 15 | 16 | We recommend setting `replicaCount` as the same number of MinIO Pods. 17 | 18 | ### Installing the Chart 19 | 20 | After configuring the `values.yaml` file, install this chart using: 21 | 22 | ```bash 23 | cd /home/warp/k8s/ 24 | helm install warp helm/ 25 | ``` 26 | 27 | The command deploys a StatefulSet with `replicaCount` number of Warp client pods and a Job with Warp Server. 28 | 29 | ### Benchmark results 30 | 31 | After Warp chart is successfully deployed, use the `kubectl get pods` command to find out the Pod related to Warp Job. For example: 32 | 33 | ```sh 34 | $ kubectl get pods 35 | NAME READY STATUS RESTARTS AGE 36 | warp-0 1/1 Running 0 11m 37 | warp-1 1/1 Running 0 11m 38 | warp-2 1/1 Running 0 11m 39 | warp-3 1/1 Running 0 11m 40 | warp-df9cs 0/1 Completed 0 11m 41 | ``` 42 | 43 | Then, use the `kubectl logs` command to get the output from Job Pod. Here you can see the benchmark results. 44 | 45 | ```sh 46 | $ kubectl logs warp-df9cs 47 | .... 48 | .... 49 | .... 50 | Operation: GET. Concurrency: 4. Hosts: 4. 51 | * Average: 448.97 MiB/s, 89.79 obj/s (4m59.883s, starting 14:01:09 UTC) 52 | 53 | Throughput by host: 54 | * http://minio-1.minio.default.svc.cluster.local:9000: Avg: 112.26 MiB/s, 22.45 obj/s (4m59.834s, starting 14:01:09 UTC) 55 | * http://minio-2.minio.default.svc.cluster.local:9000: Avg: 112.27 MiB/s, 22.45 obj/s (4m59.797s, starting 14:01:09 UTC) 56 | * http://minio-3.minio.default.svc.cluster.local:9000: Avg: 112.27 MiB/s, 22.45 obj/s (4m59.938s, starting 14:01:09 UTC) 57 | * http://minio-0.minio.default.svc.cluster.local:9000: Avg: 112.27 MiB/s, 22.45 obj/s (4m59.934s, starting 14:01:09 UTC) 58 | 59 | Aggregated Throughput, split into 299 x 1s time segments: 60 | * Fastest: 580.4MiB/s, 116.09 obj/s (1s, starting 14:01:11 UTC) 61 | * 50% Median: 471.9MiB/s, 94.38 obj/s (1s, starting 14:04:25 UTC) 62 | * Slowest: 189.4MiB/s, 37.87 obj/s (1s, starting 14:02:23 UTC) 63 | ``` 64 | -------------------------------------------------------------------------------- /k8s/helm/templates/NOTES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minio/warp/4736827d64c59b604f64fb332a062da769150441/k8s/helm/templates/NOTES.txt -------------------------------------------------------------------------------- /k8s/helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "warp.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "warp.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "warp.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Set the image tag to use. 36 | */}} 37 | {{- define "warp.imageVersion" -}} 38 | {{- default .Chart.AppVersion .Values.image.version -}} 39 | {{- end -}} 40 | 41 | {{/* 42 | Common labels 43 | */}} 44 | {{- define "warp.labels" -}} 45 | helm.sh/chart: {{ include "warp.chart" . }} 46 | {{ include "warp.selectorLabels" . }} 47 | {{- if .Chart.AppVersion }} 48 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 49 | {{- end }} 50 | app.kubernetes.io/managed-by: {{ .Release.Service }} 51 | {{- end -}} 52 | 53 | {{/* 54 | Selector labels 55 | */}} 56 | {{- define "warp.selectorLabels" -}} 57 | app.kubernetes.io/name: {{ include "warp.name" . }} 58 | app.kubernetes.io/instance: {{ .Release.Name }} 59 | {{- end -}} 60 | 61 | {{/* 62 | Create the name of the service account to use 63 | */}} 64 | {{- define "warp.serviceAccountName" -}} 65 | {{- if .Values.serviceAccount.create -}} 66 | {{ default (include "warp.fullname" .) .Values.serviceAccount.name }} 67 | {{- else -}} 68 | {{ default "default" .Values.serviceAccount.name }} 69 | {{- end -}} 70 | {{- end -}} 71 | -------------------------------------------------------------------------------- /k8s/helm/templates/job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: {{ include "warp.fullname" . }} 5 | labels: 6 | {{- include "warp.labels" . | nindent 4 }} 7 | spec: 8 | template: 9 | spec: 10 | restartPolicy: Never 11 | containers: 12 | - name: {{ include "warp.fullname" . }} 13 | image: "{{ .Values.image.repository }}:{{ include "warp.imageVersion" . }}" 14 | imagePullPolicy: {{ .Values.image.pullPolicy }} 15 | args: 16 | - "{{ .Values.warpConfiguration.operationToBenchmark }}" 17 | - "--warp-client={{ include "warp.fullname" . }}-{0...{{ sub .Values.replicaCount 1 }}}.{{ include "warp.fullname" . }}.{{ .Release.Namespace }}" 18 | {{- range $k, $v := .Values.warpJobArgs }} 19 | - --{{ $k }}={{ $v }} 20 | {{- end }} 21 | env: 22 | - name: WARP_HOST 23 | value: {{ .Values.warpConfiguration.s3ServerURL | quote }} 24 | {{- if .Values.warpConfiguration.s3ServerTLSEnabled }} 25 | - name: WARP_TLS 26 | value: "true" 27 | {{- end }} 28 | - name: WARP_REGION 29 | value: {{ .Values.warpConfiguration.s3ServerRegion | quote }} 30 | - name: WARP_ACCESS_KEY 31 | valueFrom: 32 | secretKeyRef: 33 | name: {{ include "warp.fullname" . }}-credentials 34 | key: access_key 35 | - name: WARP_SECRET_KEY 36 | valueFrom: 37 | secretKeyRef: 38 | name: {{ include "warp.fullname" . }}-credentials 39 | key: secret_key 40 | {{- if .Values.serverResources }} 41 | resources: {{- toYaml .Values.serverResources | nindent 12 }} 42 | {{- end }} 43 | {{- if .Values.securityContext }} 44 | securityContext: {{- toYaml .Values.securityContext | nindent 12 }} 45 | {{- end }} 46 | {{- if .Values.serviceAccount.create }} 47 | serviceAccountName: {{ include "warp.serviceAccountName" . }} 48 | {{- end }} 49 | {{- if .Values.podSecurityContext }} 50 | securityContext: {{- .Values.podSecurityContext | toYaml | nindent 8 }} 51 | {{- end }} 52 | {{- if .Values.affinity }} 53 | affinity: {{- .Values.affinity | toYaml | nindent 8 }} 54 | {{- end }} 55 | {{- if .Values.nodeSelector }} 56 | nodeSelector: {{- .Values.nodeSelector | toYaml | nindent 8 }} 57 | {{- end }} 58 | {{- if .Values.tolerations }} 59 | tolerations: {{- .Values.tolerations | toYaml | nindent 8 }} 60 | {{- end }} 61 | backoffLimit: 4 62 | -------------------------------------------------------------------------------- /k8s/helm/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "warp.fullname" . }}-credentials 5 | labels: 6 | {{- include "warp.labels" . | nindent 4 }} 7 | data: 8 | access_key: {{ .Values.warpConfiguration.s3AccessKey | b64enc }} 9 | secret_key: {{ .Values.warpConfiguration.s3SecretKey | b64enc }} 10 | -------------------------------------------------------------------------------- /k8s/helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "warp.fullname" . }} 5 | labels: 6 | {{- include "warp.labels" . | nindent 4 }} 7 | spec: 8 | publishNotReadyAddresses: true 9 | clusterIP: None 10 | selector: 11 | {{- include "warp.selectorLabels" . | nindent 4 }} 12 | ports: 13 | - port: {{ .Values.service.port }} 14 | name: warp 15 | -------------------------------------------------------------------------------- /k8s/helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "warp.serviceAccountName" . }} 6 | labels: 7 | {{ include "warp.labels" . | nindent 4 }} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /k8s/helm/templates/statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: {{ include "warp.fullname" . }} 5 | labels: 6 | {{- include "warp.labels" . | nindent 4 }} 7 | spec: 8 | serviceName: {{ include "warp.fullname" . }} 9 | podManagementPolicy: Parallel 10 | replicas: {{ .Values.replicaCount }} 11 | selector: 12 | matchLabels: 13 | {{- include "warp.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | name: {{ template "warp.fullname" . }} 17 | labels: 18 | {{- include "warp.selectorLabels" . | nindent 8 }} 19 | spec: 20 | containers: 21 | - name: {{ .Chart.Name }} 22 | image: "{{ .Values.image.repository }}:{{ include "warp.imageVersion" . }}" 23 | imagePullPolicy: {{ .Values.image.pullPolicy }} 24 | args: 25 | - client 26 | ports: 27 | - name: http 28 | containerPort: {{ .Values.service.port }} 29 | {{- if .Values.clientResources }} 30 | resources: {{- toYaml .Values.clientResources | nindent 12 }} 31 | {{- end }} 32 | {{- if .Values.securityContext }} 33 | securityContext: {{- toYaml .Values.securityContext | nindent 12 }} 34 | {{- end }} 35 | {{- if .Values.serviceAccount.create }} 36 | serviceAccountName: {{ include "warp.serviceAccountName" . }} 37 | {{- end }} 38 | {{- if .Values.podSecurityContext }} 39 | securityContext: {{- .Values.podSecurityContext | toYaml | nindent 8 }} 40 | {{- end }} 41 | {{- if .Values.affinity }} 42 | affinity: {{- .Values.affinity | toYaml | nindent 8 }} 43 | {{- end }} 44 | {{- if .Values.nodeSelector }} 45 | nodeSelector: {{- .Values.nodeSelector | toYaml | nindent 8 }} 46 | {{- end }} 47 | {{- if .Values.tolerations }} 48 | tolerations: {{- .Values.tolerations | toYaml | nindent 8 }} 49 | {{- end }} 50 | -------------------------------------------------------------------------------- /k8s/helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for warp. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # Number of warp client instances 6 | replicaCount: 4 7 | 8 | image: 9 | repository: minio/warp 10 | pullPolicy: IfNotPresent 11 | # Set version to use a specific release of Warp 12 | # version: latest 13 | 14 | imagePullSecrets: [] 15 | nameOverride: "" 16 | fullnameOverride: "" 17 | 18 | warpConfiguration: 19 | # MinIO or other S3 Compatible server URL 20 | s3ServerURL: minio-{0...3}.minio.default.svc.cluster.local:9000 21 | # Whether TLS enabled or not for above URL 22 | s3ServerTLSEnabled: false 23 | # Region for S3 Server 24 | s3ServerRegion: "us-east-1" 25 | # MinIO or other S3 Compatible server Access Key 26 | s3AccessKey: "minio" 27 | # MinIO or other S3 Compatible server Secret Key 28 | s3SecretKey: "minio123" 29 | # Operation to be benchmarked (get / put / delete / list / stat / mixed) 30 | operationToBenchmark: get 31 | 32 | warpJobArgs: {} 33 | # Full args can be found: https://github.com/minio/warp#usage 34 | # 35 | # Number of objects to be used 36 | # objects: 1000 37 | # 38 | # Object size to be used for benchmarks 39 | # obj.size: 10MiB 40 | # 41 | # Duration for which the benchmark will run 42 | # duration: 5m0s 43 | # 44 | # Number of parallel operations to run during benchmark 45 | # concurrent: 10 46 | # 47 | # By default operations are performed on a bucket called warp-benchmark-bucket. 48 | # This can be changed using the --bucket parameter. Do however note that the bucket 49 | # will be completely cleaned before and after each run, so it should not contain any data. 50 | # bucket: "warp-benchmark-bucket" 51 | 52 | serviceAccount: 53 | # Specifies whether a service account should be created 54 | create: true 55 | # The name of the service account to use. 56 | # If not set and create is true, a name is generated using the fullname template 57 | # name: 58 | 59 | securityContext: 60 | readOnlyRootFilesystem: true 61 | 62 | podSecurityContext: 63 | runAsNonRoot: true 64 | runAsUser: 1001 65 | fsGroup: 1001 66 | 67 | service: 68 | port: 7761 69 | 70 | serverResources: {} 71 | # limits: 72 | # cpu: 500m 73 | # memory: 512Mi 74 | # requests: 75 | # cpu: 100m 76 | # memory: 128Mi 77 | 78 | clientResources: {} 79 | # limits: 80 | # cpu: 4 81 | # memory: 512Mi 82 | # requests: 83 | # cpu: 100m 84 | # memory: 128Mi 85 | 86 | nodeSelector: {} 87 | 88 | tolerations: [] 89 | 90 | affinity: {} 91 | -------------------------------------------------------------------------------- /k8s/warp-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: warp-job 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: warp-job 10 | env: 11 | - name: WARP_ACCESS_KEY 12 | value: "minio" 13 | - name: WARP_SECRET_KEY 14 | value: "minio123" 15 | image: "minio/warp:latest" 16 | imagePullPolicy: Always 17 | args: [ "get", "--bucket", "benchmark-bucket", "--warp-client", "warp-{0...3}.warp.default.svc.cluster.local:7761", "--host", "minio-{0...3}.minio.default.svc.cluster.local:9000", "--concurrent", "64", "--obj.size", "32MiB" ] 18 | restartPolicy: Never 19 | backoffLimit: 4 20 | -------------------------------------------------------------------------------- /k8s/warp.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: warp 6 | labels: 7 | app: warp 8 | spec: 9 | publishNotReadyAddresses: true 10 | clusterIP: None 11 | ports: 12 | - port: 7761 13 | name: warp 14 | selector: 15 | app: warp 16 | --- 17 | apiVersion: apps/v1 18 | kind: StatefulSet 19 | metadata: 20 | name: warp 21 | labels: 22 | app: warp 23 | spec: 24 | serviceName: warp 25 | podManagementPolicy: Parallel 26 | replicas: 4 27 | selector: 28 | matchLabels: 29 | app: warp 30 | template: 31 | metadata: 32 | name: warp 33 | labels: 34 | app: warp 35 | spec: 36 | affinity: 37 | podAntiAffinity: 38 | requiredDuringSchedulingIgnoredDuringExecution: 39 | - labelSelector: 40 | matchExpressions: 41 | - key: app 42 | operator: In 43 | values: 44 | - warp 45 | topologyKey: "kubernetes.io/hostname" 46 | containers: 47 | - name: warp 48 | image: "minio/warp:latest" 49 | imagePullPolicy: Always 50 | args: 51 | - client 52 | ports: 53 | - name: http 54 | containerPort: 7761 55 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minio/warp/4736827d64c59b604f64fb332a062da769150441/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "os" 22 | 23 | "github.com/minio/warp/cli" 24 | ) 25 | 26 | var ( 27 | version string 28 | commit string 29 | date string 30 | ) 31 | 32 | func main() { 33 | cli.GlobalVersion = version 34 | cli.GlobalCommit = commit 35 | cli.GlobalDate = date 36 | cli.Main(os.Args) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/aggregate/collector.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2024 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package aggregate 19 | 20 | import ( 21 | "context" 22 | "math" 23 | "sync" 24 | "time" 25 | 26 | "github.com/minio/pkg/v3/console" 27 | "github.com/minio/warp/pkg/bench" 28 | ) 29 | 30 | // LiveCollector return a collector, and a channel that will return the 31 | // current aggregate on the channel whenever it is requested. 32 | func LiveCollector(ctx context.Context, updates chan UpdateReq, clientID string) bench.Collector { 33 | c := collector{ 34 | rcv: make(chan bench.Operation, 1000), 35 | } 36 | // Unbuffered, so we can send on unblock. 37 | if updates == nil { 38 | updates = make(chan UpdateReq, 1000) 39 | } 40 | c.updates = updates 41 | go func() { 42 | final := Live(c.rcv, updates, clientID) 43 | for { 44 | select { 45 | case <-ctx.Done(): 46 | return 47 | case req := <-updates: 48 | select { 49 | case req.C <- final: 50 | default: 51 | } 52 | } 53 | } 54 | }() 55 | return &c 56 | } 57 | 58 | // UpdateReq is a request for an update. 59 | // The latest will be sent on the provided channel, or nil if none is available yet. 60 | // If the provided channel blocks no update will be sent. 61 | type UpdateReq struct { 62 | C chan<- *Realtime `json:"-"` 63 | Reset bool `json:"reset"` // Does not return result. 64 | Final bool `json:"final"` // Blocks until final value is ready. 65 | } 66 | 67 | type collector struct { 68 | mu sync.Mutex 69 | rcv chan bench.Operation 70 | extra []chan<- bench.Operation 71 | updates chan<- UpdateReq 72 | doneFn []context.CancelFunc 73 | } 74 | 75 | func (c *collector) AutoTerm(ctx context.Context, op string, threshold float64, wantSamples, _ int, minDur time.Duration) context.Context { 76 | return AutoTerm(ctx, op, threshold, wantSamples, minDur, c.updates) 77 | } 78 | 79 | // AutoTerm allows to set auto-termination on a context. 80 | func AutoTerm(ctx context.Context, op string, threshold float64, wantSamples int, minDur time.Duration, updates chan<- UpdateReq) context.Context { 81 | if updates == nil { 82 | return ctx 83 | } 84 | ctx, cancel := context.WithCancel(ctx) 85 | go func() { 86 | defer cancel() 87 | ticker := time.NewTicker(time.Second) 88 | 89 | checkloop: 90 | for { 91 | select { 92 | case <-ctx.Done(): 93 | ticker.Stop() 94 | return 95 | case <-ticker.C: 96 | } 97 | respCh := make(chan *Realtime, 1) 98 | req := UpdateReq{C: respCh, Reset: false, Final: false} 99 | updates <- req 100 | resp := <-respCh 101 | if resp == nil { 102 | continue 103 | } 104 | 105 | ops := resp.ByOpType[op] 106 | if op == "" { 107 | ops = &resp.Total 108 | } 109 | if ops == nil || ops.Throughput.Segmented == nil { 110 | continue 111 | } 112 | start, end := ops.StartTime, ops.EndTime 113 | if end.Sub(start) <= minDur { 114 | // We don't have enough. 115 | continue 116 | } 117 | if len(ops.Throughput.Segmented.Segments) < wantSamples { 118 | continue 119 | } 120 | segs := ops.Throughput.Segmented.Segments 121 | // Use last segment as our base. 122 | lastSeg := segs[len(segs)-1] 123 | mb, objs := lastSeg.BPS, lastSeg.OPS 124 | // Only use the segments we are interested in. 125 | segs = segs[len(segs)-wantSamples : len(segs)-1] 126 | for _, seg := range segs { 127 | segMB, segObjs := seg.BPS, seg.OPS 128 | if mb > 0 { 129 | if math.Abs(mb-segMB) > threshold*mb { 130 | continue checkloop 131 | } 132 | continue 133 | } 134 | if math.Abs(objs-segObjs) > threshold*objs { 135 | continue checkloop 136 | } 137 | } 138 | // All checks passed. 139 | if mb > 0 { 140 | console.Eraseline() 141 | console.Printf("\rThroughput %0.01fMiB/s within %f%% for %v. Assuming stability. Terminating benchmark.\n", 142 | mb, threshold*100, 143 | time.Duration(ops.Throughput.Segmented.SegmentDurationMillis*(len(segs)+1))*time.Millisecond) 144 | } else { 145 | console.Eraseline() 146 | console.Printf("\rThroughput %0.01f objects/s within %f%% for %v. Assuming stability. Terminating benchmark.\n", 147 | objs, threshold*100, 148 | time.Duration(ops.Throughput.Segmented.SegmentDurationMillis*(len(segs)+1))*time.Millisecond) 149 | } 150 | return 151 | } 152 | }() 153 | return ctx 154 | } 155 | 156 | func (c *collector) Receiver() chan<- bench.Operation { 157 | return c.rcv 158 | } 159 | 160 | func (c *collector) AddOutput(operations ...chan<- bench.Operation) { 161 | c.extra = append(c.extra, operations...) 162 | } 163 | 164 | func (c *collector) Close() { 165 | c.mu.Lock() 166 | defer c.mu.Unlock() 167 | if c.rcv != nil { 168 | close(c.rcv) 169 | c.rcv = nil 170 | } 171 | for _, cancel := range c.doneFn { 172 | cancel() 173 | } 174 | c.doneFn = nil 175 | } 176 | -------------------------------------------------------------------------------- /pkg/aggregate/compare.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2025 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package aggregate 19 | 20 | import ( 21 | "fmt" 22 | "time" 23 | 24 | "github.com/minio/warp/pkg/bench" 25 | ) 26 | 27 | func Compare(before, after *LiveAggregate, op string) (*bench.Comparison, error) { 28 | var res bench.Comparison 29 | 30 | if before.TotalErrors > 0 || after.TotalErrors > 0 { 31 | return nil, fmt.Errorf("errors recorded in benchmark run. before: %v, after %d", before.TotalErrors, after.TotalErrors) 32 | } 33 | if after.Throughput.Segmented == nil || after.Throughput.Segmented.Segments == nil || 34 | before.Throughput.Segmented == nil || before.Throughput.Segmented.Segments == nil { 35 | return nil, fmt.Errorf("no segments found in benchmark run. before: %v, after %v", before.Throughput.Segmented.Segments, after.Throughput.Segmented.Segments) 36 | } 37 | res.Op = op 38 | as := after.Throughput.Segmented.Segments 39 | aDur := time.Duration(after.Throughput.Segmented.SegmentDurationMillis) * time.Millisecond 40 | bDur := time.Duration(after.Throughput.Segmented.SegmentDurationMillis) * time.Millisecond 41 | bs := before.Throughput.Segmented.Segments 42 | as.SortByObjsPerSec() 43 | bs.SortByObjsPerSec() 44 | res.Median.Compare(bs.Median(0.5).LongSeg(bDur), as.Median(0.5).LongSeg(aDur)) 45 | res.Slowest.Compare(bs.Median(0.0).LongSeg(bDur), as.Median(0.0).LongSeg(aDur)) 46 | res.Fastest.Compare(bs.Median(1).LongSeg(bDur), as.Median(1).LongSeg(aDur)) 47 | 48 | beforeTotals := bench.Segment{ 49 | EndsBefore: before.EndTime, 50 | Start: before.StartTime, 51 | OpType: op, 52 | Host: "", 53 | OpsStarted: before.TotalRequests, 54 | PartialOps: 0, 55 | FullOps: before.TotalObjects, 56 | OpsEnded: before.TotalRequests, 57 | Objects: float64(before.TotalObjects), 58 | Errors: before.TotalErrors, 59 | ReqAvg: float64(before.TotalRequests), 60 | TotalBytes: before.TotalBytes, 61 | ObjsPerOp: before.TotalObjects / before.TotalRequests, 62 | } 63 | 64 | afterTotals := bench.Segment{ 65 | EndsBefore: after.EndTime, 66 | Start: after.StartTime, 67 | OpType: op, 68 | Host: "", 69 | OpsStarted: after.TotalRequests, 70 | PartialOps: 0, 71 | FullOps: after.TotalObjects, 72 | OpsEnded: after.TotalRequests, 73 | Objects: float64(after.TotalObjects), 74 | Errors: after.TotalErrors, 75 | ReqAvg: float64(after.TotalRequests), 76 | TotalBytes: after.TotalBytes, 77 | ObjsPerOp: after.TotalObjects / after.TotalRequests, 78 | } 79 | res.Average.Compare(beforeTotals, afterTotals) 80 | 81 | if after.Requests != nil && before.Requests != nil { 82 | a, _ := mergeRequests(after.Requests) 83 | b, _ := mergeRequests(before.Requests) 84 | // TODO: Do multisized? 85 | if a.Requests > 0 || b.Requests > 0 { 86 | ms := float64(time.Millisecond) 87 | const round = 100 * time.Microsecond 88 | aInv := 1.0 / max(1, float64(a.MergedEntries)) 89 | bInv := 1.0 / max(1, float64(b.MergedEntries)) 90 | res.Reqs.CmpRequests = bench.CmpRequests{ 91 | AvgObjSize: a.ObjSize/int64(a.MergedEntries) - b.ObjSize/int64(b.MergedEntries), 92 | Requests: a.Requests - b.Requests, 93 | Average: time.Duration((a.DurAvgMillis*aInv - b.DurMedianMillis*bInv) * ms).Round(round), 94 | Worst: time.Duration((a.SlowestMillis - b.SlowestMillis) * ms).Round(round), 95 | Best: time.Duration((a.FastestMillis - b.FastestMillis) * ms).Round(round), 96 | Median: time.Duration((a.DurMedianMillis*aInv - b.DurMedianMillis*bInv) * ms).Round(round), 97 | P90: time.Duration((a.Dur90Millis*aInv - b.Dur90Millis*bInv) * ms).Round(round), 98 | P99: time.Duration((a.Dur99Millis*aInv - b.Dur99Millis*bInv) * ms).Round(round), 99 | StdDev: time.Duration((a.StdDev*aInv - b.StdDev*bInv) * ms).Round(round), 100 | } 101 | res.Reqs.Before = bench.CmpRequests{ 102 | AvgObjSize: b.ObjSize / int64(b.MergedEntries), 103 | Requests: b.Requests, 104 | Average: time.Duration(b.DurAvgMillis * bInv * ms).Round(round), 105 | Worst: time.Duration(b.SlowestMillis * ms).Round(round), 106 | Best: time.Duration(b.FastestMillis * ms).Round(round), 107 | Median: time.Duration(b.DurMedianMillis * bInv * ms).Round(round), 108 | P90: time.Duration(b.Dur90Millis * bInv * ms).Round(round), 109 | P99: time.Duration(b.Dur99Millis * bInv * ms).Round(round), 110 | StdDev: time.Duration(b.StdDev * bInv * ms).Round(round), 111 | } 112 | res.Reqs.After = bench.CmpRequests{ 113 | AvgObjSize: a.ObjSize / int64(a.MergedEntries), 114 | Requests: a.Requests, 115 | Average: time.Duration(a.DurAvgMillis * aInv * ms).Round(round), 116 | Worst: time.Duration(a.SlowestMillis * ms).Round(round), 117 | Best: time.Duration(a.FastestMillis * ms).Round(round), 118 | Median: time.Duration(a.DurMedianMillis * aInv * ms).Round(round), 119 | P90: time.Duration(a.Dur90Millis * aInv * ms).Round(round), 120 | P99: time.Duration(a.Dur99Millis * aInv * ms).Round(round), 121 | StdDev: time.Duration(a.StdDev * aInv * ms).Round(round), 122 | } 123 | if a.FirstByte != nil && b.FirstByte != nil { 124 | res.TTFB = b.FirstByte.AsBench(b.MergedEntries).Compare(a.FirstByte.AsBench(a.MergedEntries)) 125 | } 126 | } 127 | } 128 | 129 | return &res, nil 130 | } 131 | -------------------------------------------------------------------------------- /pkg/aggregate/mapasslice.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2025 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package aggregate 19 | 20 | import ( 21 | "bytes" 22 | "encoding/json" 23 | "sort" 24 | ) 25 | 26 | // MapAsSlice is a key-only map that is serialized as an array. 27 | type MapAsSlice map[string]struct{} 28 | 29 | // Add value 30 | func (m *MapAsSlice) Add(k string) { 31 | if *m == nil { 32 | *m = make(MapAsSlice) 33 | } 34 | mp := *m 35 | mp[k] = struct{}{} 36 | } 37 | 38 | // AddMap adds another map. 39 | func (m *MapAsSlice) AddMap(other MapAsSlice) { 40 | if *m == nil { 41 | *m = make(MapAsSlice, len(other)) 42 | } 43 | mp := *m 44 | for k := range other { 45 | mp[k] = struct{}{} 46 | } 47 | } 48 | 49 | // AddSlice adds a slice. 50 | func (m *MapAsSlice) AddSlice(other []string) { 51 | if *m == nil { 52 | *m = make(MapAsSlice, len(other)) 53 | } 54 | mp := *m 55 | for _, k := range other { 56 | mp[k] = struct{}{} 57 | } 58 | } 59 | 60 | // SetSlice replaces the value with the content of a slice. 61 | func (m *MapAsSlice) SetSlice(v []string) { 62 | *m = make(MapAsSlice, len(v)) 63 | mp := *m 64 | for _, k := range v { 65 | mp[k] = struct{}{} 66 | } 67 | } 68 | 69 | // Clone returns a clone. 70 | func (m *MapAsSlice) Clone() MapAsSlice { 71 | if m == nil { 72 | return MapAsSlice(nil) 73 | } 74 | mm := make(MapAsSlice, len(*m)) 75 | for k, v := range *m { 76 | mm[k] = v 77 | } 78 | return mm 79 | } 80 | 81 | // MarshalJSON provides output as JSON. 82 | func (m MapAsSlice) MarshalJSON() ([]byte, error) { 83 | if m == nil { 84 | return []byte("null"), nil 85 | } 86 | var dst bytes.Buffer 87 | dst.WriteByte('[') 88 | x := m.Slice() 89 | for i, k := range x { 90 | dst.WriteByte('"') 91 | json.HTMLEscape(&dst, []byte(k)) 92 | dst.WriteByte('"') 93 | if i < len(x)-1 { 94 | dst.WriteByte(',') 95 | } 96 | } 97 | dst.WriteByte(']') 98 | 99 | return dst.Bytes(), nil 100 | } 101 | 102 | // UnmarshalJSON reads an array of strings and sets them as keys in the map. 103 | func (m *MapAsSlice) UnmarshalJSON(b []byte) error { 104 | var tmp []string 105 | err := json.Unmarshal(b, &tmp) 106 | if err != nil { 107 | return err 108 | } 109 | if tmp == nil { 110 | *m = nil 111 | return nil 112 | } 113 | dst := make(MapAsSlice, len(tmp)) 114 | for _, v := range tmp { 115 | dst[v] = struct{}{} 116 | } 117 | *m = dst 118 | return nil 119 | } 120 | 121 | // Slice returns the keys as a sorted slice. 122 | func (m MapAsSlice) Slice() []string { 123 | x := make([]string, 0, len(m)) 124 | for k := range m { 125 | x = append(x, k) 126 | } 127 | sort.Strings(x) 128 | return x 129 | } 130 | -------------------------------------------------------------------------------- /pkg/aggregate/ttfb.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package aggregate 19 | 20 | import ( 21 | "fmt" 22 | "time" 23 | 24 | "github.com/minio/warp/pkg/bench" 25 | ) 26 | 27 | // TTFB contains times to first byte if applicable. 28 | type TTFB struct { 29 | AverageMillis float64 `json:"average_millis"` 30 | FastestMillis float64 `json:"fastest_millis"` 31 | P25Millis float64 `json:"p25_millis"` 32 | MedianMillis float64 `json:"median_millis"` 33 | P75Millis float64 `json:"p75_millis"` 34 | P90Millis float64 `json:"p90_millis"` 35 | P99Millis float64 `json:"p99_millis"` 36 | SlowestMillis float64 `json:"slowest_millis"` 37 | StdDevMillis float64 `json:"std_dev_millis"` 38 | PercentilesMillis *[101]float64 `json:"percentiles_millis,omitempty"` 39 | } 40 | 41 | // AsBench converts to bench.TTFB. 42 | // Provide the byN value to scale the values (typically merged count). 43 | func (t *TTFB) AsBench(byN int) bench.TTFB { 44 | if t == nil { 45 | return bench.TTFB{} 46 | } 47 | if byN == 0 { 48 | byN = 1 49 | } 50 | millisToDurF := func(millis float64) time.Duration { 51 | if millis == 0 { 52 | return 0 53 | } 54 | return time.Duration(millis * float64(time.Millisecond)) 55 | } 56 | var pct [101]time.Duration 57 | invBy := 1.0 / float64(byN) 58 | if t.PercentilesMillis != nil { 59 | for i, v := range t.PercentilesMillis { 60 | pct[i] = millisToDurF(v * invBy) 61 | } 62 | } 63 | return bench.TTFB{ 64 | Average: millisToDurF(t.AverageMillis * invBy), 65 | Worst: millisToDurF(t.SlowestMillis), 66 | Best: millisToDurF(t.FastestMillis), 67 | P25: millisToDurF(t.P25Millis * invBy), 68 | Median: millisToDurF(t.MedianMillis * invBy), 69 | P75: millisToDurF(t.P75Millis * invBy), 70 | P90: millisToDurF(t.P90Millis * invBy), 71 | P99: millisToDurF(t.P99Millis * invBy), 72 | StdDev: millisToDurF(t.StdDevMillis * invBy), 73 | Percentiles: pct, 74 | } 75 | } 76 | 77 | // String returns a human printable version of the time to first byte. 78 | func (t TTFB) String() string { 79 | if t.AverageMillis == 0 { 80 | return "" 81 | } 82 | fMilli := float64(time.Millisecond) 83 | return fmt.Sprintf("Avg: %v, Best: %v, 25th: %v, Median: %v, 75th: %v, 90th: %v, 99th: %v, Worst: %v StdDev: %v", 84 | time.Duration(t.AverageMillis*fMilli).Round(time.Millisecond), 85 | time.Duration(t.FastestMillis*fMilli).Round(time.Millisecond), 86 | time.Duration(t.P25Millis*fMilli).Round(time.Millisecond), 87 | time.Duration(t.MedianMillis*fMilli).Round(time.Millisecond), 88 | time.Duration(t.P75Millis*fMilli).Round(time.Millisecond), 89 | time.Duration(t.P90Millis*fMilli).Round(time.Millisecond), 90 | time.Duration(t.P99Millis*fMilli).Round(time.Millisecond), 91 | time.Duration(t.SlowestMillis*fMilli).Round(time.Millisecond), 92 | time.Duration(t.StdDevMillis*fMilli).Round(time.Millisecond)) 93 | } 94 | 95 | func (t *TTFB) add(other TTFB) { 96 | t.AverageMillis += other.AverageMillis 97 | t.MedianMillis += other.MedianMillis 98 | if other.FastestMillis != 0 { 99 | // Deal with 0 value being the best always. 100 | t.FastestMillis = min(t.FastestMillis, other.FastestMillis) 101 | if t.FastestMillis == 0 { 102 | t.FastestMillis = other.FastestMillis 103 | } 104 | } 105 | t.SlowestMillis = max(t.SlowestMillis, other.SlowestMillis) 106 | t.P25Millis += other.P25Millis 107 | t.P75Millis += other.P75Millis 108 | t.P90Millis += other.P90Millis 109 | t.P99Millis += other.P99Millis 110 | t.StdDevMillis += other.StdDevMillis 111 | } 112 | 113 | func (t TTFB) StringByN(n int) string { 114 | if t.AverageMillis == 0 || n == 0 { 115 | return "" 116 | } 117 | // rounder... 118 | fMilli := float64(time.Millisecond) 119 | fn := 1.0 / float64(n) 120 | return fmt.Sprintf("Avg: %v, Best: %v, 25th: %v, Median: %v, 75th: %v, 90th: %v, 99th: %v, Worst: %v StdDev: %v", 121 | time.Duration(t.AverageMillis*fMilli*fn).Round(time.Millisecond), 122 | time.Duration(t.FastestMillis*fMilli).Round(time.Millisecond), 123 | time.Duration(t.P25Millis*fMilli*fn).Round(time.Millisecond), 124 | time.Duration(t.MedianMillis*fMilli*fn).Round(time.Millisecond), 125 | time.Duration(t.P75Millis*fMilli*fn).Round(time.Millisecond), 126 | time.Duration(t.P90Millis*fMilli*fn).Round(time.Millisecond), 127 | time.Duration(t.P99Millis*fMilli*fn).Round(time.Millisecond), 128 | time.Duration(t.SlowestMillis*fMilli).Round(time.Millisecond), 129 | time.Duration(t.StdDevMillis*fMilli*fn).Round(time.Millisecond)) 130 | } 131 | 132 | // TtfbFromBench converts from bench.TTFB 133 | func TtfbFromBench(t bench.TTFB) *TTFB { 134 | if t.Average <= 0 { 135 | return nil 136 | } 137 | t2 := TTFB{ 138 | AverageMillis: durToMillisF(t.Average), 139 | SlowestMillis: durToMillisF(t.Worst), 140 | P25Millis: durToMillisF(t.P25), 141 | MedianMillis: durToMillisF(t.Median), 142 | P75Millis: durToMillisF(t.P75), 143 | P90Millis: durToMillisF(t.P90), 144 | P99Millis: durToMillisF(t.P99), 145 | StdDevMillis: durToMillisF(t.StdDev), 146 | FastestMillis: durToMillisF(t.Best), 147 | } 148 | t2.PercentilesMillis = &[101]float64{} 149 | for i, v := range t.Percentiles[:] { 150 | t2.PercentilesMillis[i] = durToMillisF(v) 151 | } 152 | return &t2 153 | } 154 | -------------------------------------------------------------------------------- /pkg/bench/analyze_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "bytes" 22 | "os" 23 | "testing" 24 | "time" 25 | 26 | "github.com/klauspost/compress/zstd" 27 | ) 28 | 29 | var zstdDec, _ = zstd.NewReader(nil) 30 | 31 | func TestOperations_Segment(t *testing.T) { 32 | b, err := os.ReadFile("testdata/warp-benchdata-get.csv.zst") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | b, err = zstdDec.DecodeAll(b, nil) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | ops, err := OperationsFromCSV(bytes.NewBuffer(b), false, 0, 0, t.Logf) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | for typ, ops := range ops.SortSplitByOpType() { 45 | segs := ops.Segment(SegmentOptions{ 46 | From: time.Time{}, 47 | PerSegDuration: time.Second, 48 | AllThreads: true, 49 | }) 50 | 51 | var buf bytes.Buffer 52 | err := segs.Print(&buf) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | segs.SortByThroughput() 58 | totals := ops.Total(true) 59 | ttfb := ops.TTFB(ops.ActiveTimeRange(true)) 60 | 61 | t.Log("Operation type:", typ) 62 | t.Log("OpErrors:", len(ops.Errors())) 63 | t.Log("Fastest:", segs.Median(1)) 64 | t.Log("Average:", totals) 65 | t.Log("50% Median:", segs.Median(0.5)) 66 | t.Log("Slowest:", segs.Median(0.0)) 67 | if ttfb.Average > 0 { 68 | t.Log("Time To First Byte:", ttfb) 69 | } 70 | t.Log(buf.String()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/bench/category.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2024 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "math/bits" 22 | "strings" 23 | ) 24 | 25 | //go:generate stringer -type Category -trimprefix=Cat $GOFILE 26 | 27 | // A Category allows requests to be separated into different categories. 28 | type Category uint8 29 | 30 | const ( 31 | // CatCacheMiss means that caching was detected, but the object missed the cache. 32 | CatCacheMiss Category = iota 33 | 34 | // CatCacheHit means that caching was detected and the object was cached. 35 | CatCacheHit 36 | 37 | catLength 38 | ) 39 | 40 | // Categories is a bitfield that represents potentially several categories. 41 | type Categories uint64 42 | 43 | func NewCategories(c ...Category) Categories { 44 | var cs Categories 45 | for _, cat := range c { 46 | cs |= 1 << cat 47 | } 48 | return cs 49 | } 50 | 51 | // Split returns the categories 52 | func (c Categories) Split() []Category { 53 | if c == 0 { 54 | return nil 55 | } 56 | res := make([]Category, 0, bits.OnesCount64(uint64(c))) 57 | for i := Category(0); c != 0 && i < catLength; i++ { 58 | if c&1 == 1 { 59 | res = append(res, i) 60 | } 61 | c >>= 1 62 | } 63 | return res 64 | } 65 | 66 | func (c Categories) String() string { 67 | cs := c.Split() 68 | res := make([]string, len(cs)) 69 | for i, c := range cs { 70 | res[i] = c.String() 71 | } 72 | return strings.Join(res, ",") 73 | } 74 | -------------------------------------------------------------------------------- /pkg/bench/category_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Category -trimprefix=Cat category.go"; DO NOT EDIT. 2 | 3 | package bench 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[CatCacheMiss-0] 12 | _ = x[CatCacheHit-1] 13 | _ = x[catLength-2] 14 | } 15 | 16 | const _Category_name = "CacheMissCacheHitcatLength" 17 | 18 | var _Category_index = [...]uint8{0, 9, 17, 26} 19 | 20 | func (i Category) String() string { 21 | if i >= Category(len(_Category_index)-1) { 22 | return "Category(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _Category_name[_Category_index[i]:_Category_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /pkg/bench/collector.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2023 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "context" 22 | "math" 23 | "sync" 24 | "time" 25 | 26 | "github.com/minio/pkg/v3/console" 27 | ) 28 | 29 | type Collector interface { 30 | // AutoTerm will check if throughput is within 'threshold' (0 -> ) for wantSamples, 31 | // when the current operations are split into 'splitInto' segments. 32 | // The minimum duration for the calculation can be set as well. 33 | // Segment splitting may cause less than this duration to be used. 34 | AutoTerm(ctx context.Context, op string, threshold float64, wantSamples, splitInto int, minDur time.Duration) context.Context 35 | 36 | // Receiver returns the receiver of input 37 | Receiver() chan<- Operation 38 | 39 | // AddOutput allows to add additional inputs. 40 | AddOutput(...chan<- Operation) 41 | 42 | // Close the collector 43 | Close() 44 | } 45 | 46 | type OpsCollector func() Operations 47 | 48 | func EmptyOpsCollector() Operations { 49 | return Operations{} 50 | } 51 | 52 | type collector struct { 53 | rcv chan Operation 54 | ops Operations 55 | rcvWg sync.WaitGroup 56 | extra []chan<- Operation 57 | // The mutex protects the ops above. 58 | // Once ops have been added, they should no longer be modified. 59 | opsMu sync.Mutex 60 | } 61 | 62 | // NewOpsCollector returns a collector that will collect all operations in memory. 63 | // After calling Close the returned function can be used to retrieve the operations. 64 | func NewOpsCollector() (Collector, OpsCollector) { 65 | r := &collector{ 66 | ops: make(Operations, 0, 10000), 67 | rcv: make(chan Operation, 1000), 68 | } 69 | r.rcvWg.Add(1) 70 | go func() { 71 | defer r.rcvWg.Done() 72 | for op := range r.rcv { 73 | for _, ch := range r.extra { 74 | ch <- op 75 | } 76 | r.opsMu.Lock() 77 | r.ops = append(r.ops, op) 78 | r.opsMu.Unlock() 79 | } 80 | }() 81 | return r, func() Operations { 82 | r.Close() 83 | return r.ops 84 | } 85 | } 86 | 87 | // NewNullCollector collects operations, but discards them. 88 | func NewNullCollector() Collector { 89 | r := &collector{ 90 | ops: make(Operations, 0), 91 | rcv: make(chan Operation, 1000), 92 | } 93 | r.rcvWg.Add(1) 94 | go func() { 95 | defer r.rcvWg.Done() 96 | for op := range r.rcv { 97 | for _, ch := range r.extra { 98 | ch <- op 99 | } 100 | } 101 | }() 102 | return r 103 | } 104 | 105 | // AutoTerm will check if throughput is within 'threshold' (0 -> ) for wantSamples, 106 | // when the current operations are split into 'splitInto' segments. 107 | // The minimum duration for the calculation can be set as well. 108 | // Segment splitting may cause less than this duration to be used. 109 | func (c *collector) AutoTerm(ctx context.Context, op string, threshold float64, wantSamples, splitInto int, minDur time.Duration) context.Context { 110 | if wantSamples >= splitInto { 111 | panic("wantSamples >= splitInto") 112 | } 113 | if splitInto == 0 { 114 | panic("splitInto == 0 ") 115 | } 116 | ctx, cancel := context.WithCancel(ctx) 117 | go func() { 118 | defer cancel() 119 | ticker := time.NewTicker(time.Second) 120 | 121 | checkloop: 122 | for { 123 | select { 124 | case <-ctx.Done(): 125 | ticker.Stop() 126 | return 127 | case <-ticker.C: 128 | } 129 | // Time to check if we should terminate. 130 | c.opsMu.Lock() 131 | // copies 132 | ops := c.ops.FilterByOp(op) 133 | c.opsMu.Unlock() 134 | start, end := ops.ActiveTimeRange(true) 135 | if end.Sub(start) <= minDur*time.Duration(splitInto)/time.Duration(wantSamples) { 136 | // We don't have enough. 137 | continue 138 | } 139 | segs := ops.Segment(SegmentOptions{ 140 | From: start, 141 | PerSegDuration: end.Sub(start) / time.Duration(splitInto), 142 | AllThreads: true, 143 | }) 144 | if len(segs) < wantSamples { 145 | continue 146 | } 147 | // Use last segment as our base. 148 | mb, _, objs := segs[len(segs)-1].SpeedPerSec() 149 | // Only use the segments we are interested in. 150 | segs = segs[len(segs)-wantSamples : len(segs)-1] 151 | for _, seg := range segs { 152 | segMB, _, segObjs := seg.SpeedPerSec() 153 | if mb > 0 { 154 | if math.Abs(mb-segMB) > threshold*mb { 155 | continue checkloop 156 | } 157 | continue 158 | } 159 | if math.Abs(objs-segObjs) > threshold*objs { 160 | continue checkloop 161 | } 162 | } 163 | // All checks passed. 164 | if mb > 0 { 165 | console.Eraseline() 166 | console.Printf("\rThroughput %0.01fMiB/s within %f%% for %v. Assuming stability. Terminating benchmark.\n", 167 | mb, threshold*100, 168 | segs[0].Duration().Round(time.Millisecond)*time.Duration(len(segs)+1)) 169 | } else { 170 | console.Eraseline() 171 | console.Printf("\rThroughput %0.01f objects/s within %f%% for %v. Assuming stability. Terminating benchmark.\n", 172 | objs, threshold*100, 173 | segs[0].Duration().Round(time.Millisecond)*time.Duration(len(segs)+1)) 174 | } 175 | return 176 | } 177 | }() 178 | return ctx 179 | } 180 | 181 | func (c *collector) Receiver() chan<- Operation { 182 | return c.rcv 183 | } 184 | 185 | func (c *collector) Close() { 186 | if c.rcv != nil { 187 | close(c.rcv) 188 | c.rcvWg.Wait() 189 | c.rcv = nil 190 | for _, ch := range c.extra { 191 | close(ch) 192 | } 193 | c.extra = nil 194 | } 195 | } 196 | 197 | func (c *collector) AddOutput(x ...chan<- Operation) { 198 | c.extra = append(c.extra, x...) 199 | } 200 | -------------------------------------------------------------------------------- /pkg/bench/csv.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "strings" 22 | "unicode" 23 | "unicode/utf8" 24 | ) 25 | 26 | // fieldNeedsQuotes reports whether our field must be enclosed in quotes. 27 | // Fields with a Comma, fields with a quote or newline, and 28 | // fields which start with a space must be enclosed in quotes. 29 | func fieldNeedsQuotes(field string) bool { 30 | const comma = '\t' 31 | if field == "" { 32 | return false 33 | } 34 | if field == `\.` || strings.ContainsRune(field, comma) || strings.ContainsAny(field, "\"\r\n") { 35 | return true 36 | } 37 | 38 | r1, _ := utf8.DecodeRuneInString(field) 39 | return unicode.IsSpace(r1) 40 | } 41 | 42 | func csvEscapeString(field string) string { 43 | if !fieldNeedsQuotes(field) { 44 | return field 45 | } 46 | var w strings.Builder 47 | w.WriteByte('"') 48 | 49 | for len(field) > 0 { 50 | // Search for special characters. 51 | i := strings.IndexAny(field, "\"\r\n") 52 | if i < 0 { 53 | i = len(field) 54 | } 55 | 56 | // Copy verbatim everything before the special character. 57 | w.WriteString(field[:i]) 58 | field = field[i:] 59 | 60 | // Encode the special character. 61 | if len(field) > 0 { 62 | switch field[0] { 63 | case '"': 64 | _, _ = w.WriteString(`""`) 65 | case '\r': 66 | _ = w.WriteByte('\r') 67 | case '\n': 68 | _ = w.WriteByte('\n') 69 | } 70 | field = field[1:] 71 | } 72 | } 73 | w.WriteByte('"') 74 | return w.String() 75 | } 76 | -------------------------------------------------------------------------------- /pkg/bench/fanout.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2023 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "net/http" 24 | "sync" 25 | "time" 26 | 27 | "github.com/minio/minio-go/v7" 28 | ) 29 | 30 | // Fanout benchmarks upload speed. 31 | type Fanout struct { 32 | Common 33 | Copies int 34 | prefixes map[string]struct{} 35 | } 36 | 37 | // Prepare will create an empty bucket or delete any content already there. 38 | func (u *Fanout) Prepare(ctx context.Context) error { 39 | return u.createEmptyBucket(ctx) 40 | } 41 | 42 | // Start will execute the main benchmark. 43 | // Operations should begin executing when the start channel is closed. 44 | func (u *Fanout) Start(ctx context.Context, wait chan struct{}) error { 45 | var wg sync.WaitGroup 46 | wg.Add(u.Concurrency) 47 | c := u.Collector 48 | if u.AutoTermDur > 0 { 49 | ctx = c.AutoTerm(ctx, http.MethodPost, u.AutoTermScale, autoTermCheck, autoTermSamples, u.AutoTermDur) 50 | } 51 | u.prefixes = make(map[string]struct{}, u.Concurrency) 52 | 53 | // Non-terminating context. 54 | nonTerm := context.Background() 55 | 56 | for i := 0; i < u.Concurrency; i++ { 57 | src := u.Source() 58 | u.prefixes[src.Prefix()] = struct{}{} 59 | go func(i int) { 60 | rcv := c.Receiver() 61 | defer wg.Done() 62 | opts := minio.PutObjectFanOutRequest{ 63 | Entries: make([]minio.PutObjectFanOutEntry, u.Copies), 64 | Checksum: minio.Checksum{}, 65 | SSE: nil, 66 | } 67 | done := ctx.Done() 68 | 69 | <-wait 70 | for { 71 | select { 72 | case <-done: 73 | return 74 | default: 75 | } 76 | obj := src.Object() 77 | for i := range opts.Entries { 78 | opts.Entries[i] = minio.PutObjectFanOutEntry{ 79 | Key: fmt.Sprintf("%s/copy-%d.ext", obj.Name, i), 80 | UserMetadata: u.PutOpts.UserMetadata, 81 | UserTags: u.PutOpts.UserTags, 82 | ContentType: obj.ContentType, 83 | ContentEncoding: u.PutOpts.ContentEncoding, 84 | ContentDisposition: u.PutOpts.ContentDisposition, 85 | ContentLanguage: u.PutOpts.ContentLanguage, 86 | CacheControl: u.PutOpts.CacheControl, 87 | } 88 | } 89 | client, cldone := u.Client() 90 | op := Operation{ 91 | OpType: http.MethodPost, 92 | Thread: uint16(i), 93 | Size: obj.Size * int64(u.Copies), 94 | ObjPerOp: u.Copies, 95 | File: obj.Name, 96 | Endpoint: client.EndpointURL().String(), 97 | } 98 | 99 | op.Start = time.Now() 100 | res, err := client.PutObjectFanOut(nonTerm, u.Bucket, obj.Reader, opts) 101 | op.End = time.Now() 102 | if err != nil { 103 | u.Error("upload error: ", err) 104 | op.Err = err.Error() 105 | } 106 | 107 | var firstErr string 108 | nErrs := 0 109 | for _, r := range res { 110 | if r.Error != "" { 111 | if firstErr == "" { 112 | firstErr = r.Error 113 | } 114 | nErrs++ 115 | } 116 | } 117 | if op.Err == "" && nErrs > 0 { 118 | op.Err = fmt.Sprintf("%d copies failed. First error: %v", nErrs, firstErr) 119 | } 120 | 121 | if len(res) != u.Copies && op.Err == "" { 122 | err := fmt.Sprint("short upload. want:", u.Copies, " copies, got:", len(res)) 123 | if op.Err == "" { 124 | op.Err = err 125 | } 126 | u.Error(err) 127 | } 128 | 129 | cldone() 130 | rcv <- op 131 | } 132 | }(i) 133 | } 134 | wg.Wait() 135 | return nil 136 | } 137 | 138 | // Cleanup deletes everything uploaded to the bucket. 139 | func (u *Fanout) Cleanup(ctx context.Context) { 140 | pf := make([]string, 0, len(u.prefixes)) 141 | for p := range u.prefixes { 142 | pf = append(pf, p) 143 | } 144 | u.deleteAllInBucket(ctx, pf...) 145 | } 146 | -------------------------------------------------------------------------------- /pkg/bench/multipart_put.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/minio/minio-go/v7" 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | // MultipartPut benchmarks multipart upload speed. 15 | type MultipartPut struct { 16 | Common 17 | 18 | PartsNumber int 19 | PartsConcurrency int 20 | } 21 | 22 | // Prepare for the benchmark run 23 | func (g *MultipartPut) Prepare(ctx context.Context) error { 24 | return g.createEmptyBucket(ctx) 25 | } 26 | 27 | // Start will execute the main benchmark. 28 | // Operations should begin executing when the start channel is closed. 29 | func (g *MultipartPut) Start(ctx context.Context, wait chan struct{}) error { 30 | eg, ctx := errgroup.WithContext(ctx) 31 | c := g.Collector 32 | if g.AutoTermDur > 0 { 33 | ctx = c.AutoTerm(ctx, http.MethodPut, g.AutoTermScale, autoTermCheck, autoTermSamples, g.AutoTermDur) 34 | } 35 | 36 | for i := 0; i < g.Concurrency; i++ { 37 | thread := uint16(i) 38 | eg.Go(func() error { 39 | <-wait 40 | 41 | for ctx.Err() == nil { 42 | objectName := g.Source().Object().Name 43 | 44 | uploadID, err := g.createMultupartUpload(ctx, objectName) 45 | if errors.Is(err, context.Canceled) { 46 | return nil 47 | } 48 | if err != nil { 49 | g.Error("create multipart upload error:", err) 50 | continue 51 | } 52 | 53 | err = g.uploadParts(ctx, thread, objectName, uploadID) 54 | if errors.Is(err, context.Canceled) { 55 | return nil 56 | } 57 | if err != nil { 58 | g.Error("upload parts error:", err) 59 | continue 60 | } 61 | 62 | err = g.completeMultipartUpload(ctx, objectName, uploadID) 63 | if err != nil { 64 | g.Error("complete multipart upload") 65 | } 66 | } 67 | return nil 68 | }) 69 | } 70 | return eg.Wait() 71 | } 72 | 73 | // Cleanup up after the benchmark run. 74 | func (g *MultipartPut) Cleanup(ctx context.Context) { 75 | g.deleteAllInBucket(ctx, "") 76 | } 77 | 78 | func (g *MultipartPut) createMultupartUpload(ctx context.Context, objectName string) (string, error) { 79 | if err := g.rpsLimit(ctx); err != nil { 80 | return "", err 81 | } 82 | 83 | // Non-terminating context. 84 | nonTerm := context.Background() 85 | 86 | client, done := g.Client() 87 | defer done() 88 | c := minio.Core{Client: client} 89 | return c.NewMultipartUpload(nonTerm, g.Bucket, objectName, g.PutOpts) 90 | } 91 | 92 | func (g *MultipartPut) uploadParts(ctx context.Context, thread uint16, objectName, uploadID string) error { 93 | partIdxCh := make(chan int, g.PartsNumber) 94 | for i := 0; i < g.PartsNumber; i++ { 95 | partIdxCh <- i + 1 96 | } 97 | close(partIdxCh) 98 | 99 | eg, ctx := errgroup.WithContext(ctx) 100 | 101 | // Non-terminating context. 102 | nonTerm := context.Background() 103 | 104 | for i := 0; i < g.PartsConcurrency; i++ { 105 | eg.Go(func() error { 106 | i := i 107 | for ctx.Err() == nil { 108 | var partIdx int 109 | var ok bool 110 | select { 111 | case partIdx, ok = <-partIdxCh: 112 | if !ok { 113 | return nil 114 | } 115 | case <-ctx.Done(): 116 | continue 117 | } 118 | 119 | if err := g.rpsLimit(ctx); err != nil { 120 | return err 121 | } 122 | 123 | obj := g.Source().Object() 124 | client, done := g.Client() 125 | defer done() 126 | core := minio.Core{Client: client} 127 | op := Operation{ 128 | OpType: "PUTPART", 129 | Thread: thread*uint16(g.PartsConcurrency) + uint16(i), 130 | Size: obj.Size, 131 | File: obj.Name, 132 | ObjPerOp: 1, 133 | Endpoint: client.EndpointURL().String(), 134 | } 135 | if g.DiscardOutput { 136 | op.File = "" 137 | } 138 | 139 | opts := minio.PutObjectPartOptions{ 140 | SSE: g.PutOpts.ServerSideEncryption, 141 | DisableContentSha256: g.PutOpts.DisableContentSha256, 142 | } 143 | 144 | op.Start = time.Now() 145 | res, err := core.PutObjectPart(nonTerm, g.Bucket, objectName, uploadID, partIdx, obj.Reader, obj.Size, opts) 146 | op.End = time.Now() 147 | if err != nil { 148 | err := fmt.Errorf("upload error: %w", err) 149 | g.Error(err) 150 | return err 151 | } 152 | 153 | if res.Size != obj.Size && op.Err == "" { 154 | err := fmt.Sprint("short upload. want:", obj.Size, ", got:", res.Size) 155 | if op.Err == "" { 156 | op.Err = err 157 | } 158 | g.Error(err) 159 | } 160 | 161 | g.Collector.Receiver() <- op 162 | } 163 | 164 | return nil 165 | }) 166 | } 167 | 168 | return eg.Wait() 169 | } 170 | 171 | func (g *MultipartPut) completeMultipartUpload(_ context.Context, objectName, uploadID string) error { 172 | // Non-terminating context. 173 | nonTerm := context.Background() 174 | 175 | cl, done := g.Client() 176 | c := minio.Core{Client: cl} 177 | defer done() 178 | _, err := c.CompleteMultipartUpload(nonTerm, g.Bucket, objectName, uploadID, nil, g.PutOpts) 179 | return err 180 | } 181 | -------------------------------------------------------------------------------- /pkg/bench/put.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "fmt" 24 | "io" 25 | "mime/multipart" 26 | "net/http" 27 | "sync" 28 | "time" 29 | 30 | "github.com/minio/minio-go/v7" 31 | "github.com/minio/warp/pkg/generator" 32 | ) 33 | 34 | // Put benchmarks upload speed. 35 | type Put struct { 36 | Common 37 | PostObject bool 38 | prefixes map[string]struct{} 39 | cl *http.Client 40 | } 41 | 42 | // Prepare will create an empty bucket or delete any content already there. 43 | func (u *Put) Prepare(ctx context.Context) error { 44 | if u.PostObject { 45 | u.cl = &http.Client{ 46 | Transport: u.Transport, 47 | } 48 | } 49 | return u.createEmptyBucket(ctx) 50 | } 51 | 52 | // Start will execute the main benchmark. 53 | // Operations should begin executing when the start channel is closed. 54 | func (u *Put) Start(ctx context.Context, wait chan struct{}) error { 55 | var wg sync.WaitGroup 56 | wg.Add(u.Concurrency) 57 | c := u.Collector 58 | if u.AutoTermDur > 0 { 59 | ctx = c.AutoTerm(ctx, http.MethodPut, u.AutoTermScale, autoTermCheck, autoTermSamples, u.AutoTermDur) 60 | } 61 | u.prefixes = make(map[string]struct{}, u.Concurrency) 62 | 63 | // Non-terminating context. 64 | nonTerm := context.Background() 65 | 66 | for i := 0; i < u.Concurrency; i++ { 67 | src := u.Source() 68 | u.prefixes[src.Prefix()] = struct{}{} 69 | go func(i int) { 70 | rcv := c.Receiver() 71 | defer wg.Done() 72 | 73 | // Copy usermetadata and usertags per concurrent thread. 74 | opts := u.PutOpts 75 | opts.UserMetadata = make(map[string]string, len(u.PutOpts.UserMetadata)) 76 | opts.UserTags = make(map[string]string, len(u.PutOpts.UserTags)) 77 | for k, v := range u.PutOpts.UserMetadata { 78 | opts.UserMetadata[k] = v 79 | } 80 | for k, v := range u.PutOpts.UserTags { 81 | opts.UserTags[k] = v 82 | } 83 | 84 | done := ctx.Done() 85 | 86 | <-wait 87 | for { 88 | select { 89 | case <-done: 90 | return 91 | default: 92 | } 93 | 94 | if u.rpsLimit(ctx) != nil { 95 | return 96 | } 97 | 98 | obj := src.Object() 99 | opts.ContentType = obj.ContentType 100 | client, cldone := u.Client() 101 | op := Operation{ 102 | OpType: http.MethodPut, 103 | Thread: uint16(i), 104 | Size: obj.Size, 105 | ObjPerOp: 1, 106 | File: obj.Name, 107 | Endpoint: client.EndpointURL().String(), 108 | } 109 | 110 | op.Start = time.Now() 111 | var err error 112 | var res minio.UploadInfo 113 | if !u.PostObject { 114 | res, err = client.PutObject(nonTerm, u.Bucket, obj.Name, obj.Reader, obj.Size, opts) 115 | } else { 116 | op.OpType = http.MethodPost 117 | var verID string 118 | verID, err = u.postPolicy(ctx, client, u.Bucket, obj) 119 | if err == nil { 120 | res.Size = obj.Size 121 | res.VersionID = verID 122 | } 123 | } 124 | op.End = time.Now() 125 | if err != nil { 126 | u.Error("upload error: ", err) 127 | op.Err = err.Error() 128 | } 129 | obj.VersionID = res.VersionID 130 | 131 | if res.Size != obj.Size && op.Err == "" { 132 | err := fmt.Sprint("short upload. want:", obj.Size, ", got:", res.Size) 133 | if op.Err == "" { 134 | op.Err = err 135 | } 136 | u.Error(err) 137 | } 138 | op.Size = res.Size 139 | cldone() 140 | rcv <- op 141 | } 142 | }(i) 143 | } 144 | wg.Wait() 145 | return nil 146 | } 147 | 148 | // Cleanup deletes everything uploaded to the bucket. 149 | func (u *Put) Cleanup(ctx context.Context) { 150 | pf := make([]string, 0, len(u.prefixes)) 151 | for p := range u.prefixes { 152 | pf = append(pf, p) 153 | } 154 | u.deleteAllInBucket(ctx, pf...) 155 | } 156 | 157 | // postPolicy will upload using https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html API. 158 | func (u *Put) postPolicy(ctx context.Context, c *minio.Client, bucket string, obj *generator.Object) (versionID string, err error) { 159 | pp := minio.NewPostPolicy() 160 | pp.SetEncryption(u.PutOpts.ServerSideEncryption) 161 | err = errors.Join( 162 | pp.SetContentType(obj.ContentType), 163 | pp.SetBucket(bucket), 164 | pp.SetKey(obj.Name), 165 | pp.SetContentLengthRange(obj.Size, obj.Size), 166 | pp.SetExpires(time.Now().Add(24*time.Hour)), 167 | ) 168 | if err != nil { 169 | return "", err 170 | } 171 | url, form, err := c.PresignedPostPolicy(ctx, pp) 172 | if err != nil { 173 | return "", err 174 | } 175 | pr, pw := io.Pipe() 176 | defer pr.Close() 177 | writer := multipart.NewWriter(pw) 178 | go func() { 179 | for k, v := range form { 180 | if err := writer.WriteField(k, v); err != nil { 181 | pw.CloseWithError(err) 182 | return 183 | } 184 | } 185 | ff, err := writer.CreateFormFile("file", obj.Name) 186 | if err != nil { 187 | pw.CloseWithError(err) 188 | return 189 | } 190 | _, err = io.Copy(ff, obj.Reader) 191 | if err != nil { 192 | pw.CloseWithError(err) 193 | return 194 | } 195 | pw.CloseWithError(writer.Close()) 196 | }() 197 | 198 | req, err := http.NewRequest(http.MethodPost, url.String(), pr) 199 | if err != nil { 200 | return "", err 201 | } 202 | req.Header.Set("Content-Type", writer.FormDataContentType()) 203 | 204 | // make POST request with form data 205 | resp, err := u.cl.Do(req) 206 | if err != nil { 207 | return "", err 208 | } 209 | if resp.Body != nil { 210 | defer resp.Body.Close() 211 | } 212 | if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { 213 | return "", fmt.Errorf("unexpected status code: (%d) %s", resp.StatusCode, resp.Status) 214 | } 215 | 216 | return resp.Header.Get("x-amz-version-id"), nil 217 | } 218 | -------------------------------------------------------------------------------- /pkg/bench/retention.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "math/rand" 24 | "net/http" 25 | "sync" 26 | "time" 27 | 28 | "github.com/minio/minio-go/v7" 29 | "github.com/minio/warp/pkg/generator" 30 | ) 31 | 32 | // Retention benchmarks download speed. 33 | type Retention struct { 34 | Common 35 | objects generator.Objects 36 | 37 | CreateObjects int 38 | Versions int 39 | } 40 | 41 | // Prepare will create an empty bucket or delete any content already there 42 | // and upload a number of objects. 43 | func (g *Retention) Prepare(ctx context.Context) error { 44 | if err := g.createEmptyBucket(ctx); err != nil { 45 | return err 46 | } 47 | cl, done := g.Client() 48 | if !g.Versioned { 49 | err := cl.EnableVersioning(ctx, g.Bucket) 50 | if err != nil { 51 | return err 52 | } 53 | g.Versioned = true 54 | done() 55 | } 56 | 57 | src := g.Source() 58 | g.UpdateStatus(fmt.Sprint("Uploading ", g.CreateObjects, " objects with ", g.Versions, " versions each of ", src.String())) 59 | var wg sync.WaitGroup 60 | wg.Add(g.Concurrency) 61 | objs := splitObjs(g.CreateObjects, g.Concurrency) 62 | var groupErr error 63 | var mu sync.Mutex 64 | 65 | for i, obj := range objs { 66 | go func(i int, obj []struct{}) { 67 | defer wg.Done() 68 | src := g.Source() 69 | 70 | for range obj { 71 | opts := g.PutOpts 72 | rcv := g.Collector.Receiver() 73 | done := ctx.Done() 74 | 75 | select { 76 | case <-done: 77 | return 78 | default: 79 | } 80 | 81 | if g.rpsLimit(ctx) != nil { 82 | return 83 | } 84 | 85 | obj := src.Object() 86 | name := obj.Name 87 | for ver := 0; ver < g.Versions; ver++ { 88 | // New input for each version 89 | obj := src.Object() 90 | obj.Name = name 91 | client, cldone := g.Client() 92 | op := Operation{ 93 | OpType: http.MethodPut, 94 | Thread: uint16(i), 95 | Size: obj.Size, 96 | File: obj.Name, 97 | ObjPerOp: 1, 98 | Endpoint: client.EndpointURL().String(), 99 | } 100 | 101 | opts.ContentType = obj.ContentType 102 | op.Start = time.Now() 103 | res, err := client.PutObject(ctx, g.Bucket, obj.Name, obj.Reader, obj.Size, opts) 104 | op.End = time.Now() 105 | if err != nil { 106 | err := fmt.Errorf("upload error: %w", err) 107 | g.Error(err) 108 | mu.Lock() 109 | if groupErr == nil { 110 | groupErr = err 111 | } 112 | mu.Unlock() 113 | return 114 | } 115 | obj.VersionID = res.VersionID 116 | if res.Size != obj.Size { 117 | err := fmt.Errorf("short upload. want: %d, got %d", obj.Size, res.Size) 118 | g.Error(err) 119 | mu.Lock() 120 | if groupErr == nil { 121 | groupErr = err 122 | } 123 | mu.Unlock() 124 | return 125 | } 126 | cldone() 127 | mu.Lock() 128 | obj.Reader = nil 129 | g.objects = append(g.objects, *obj) 130 | g.prepareProgress(float64(len(g.objects)) / float64(g.CreateObjects*g.Versions)) 131 | mu.Unlock() 132 | rcv <- op 133 | } 134 | } 135 | }(i, obj) 136 | } 137 | wg.Wait() 138 | return groupErr 139 | } 140 | 141 | // Start will execute the main benchmark. 142 | // Operations should begin executing when the start channel is closed. 143 | func (g *Retention) Start(ctx context.Context, wait chan struct{}) error { 144 | var wg sync.WaitGroup 145 | wg.Add(g.Concurrency) 146 | c := g.Collector 147 | if g.AutoTermDur > 0 { 148 | ctx = c.AutoTerm(ctx, http.MethodGet, g.AutoTermScale, autoTermCheck, autoTermSamples, g.AutoTermDur) 149 | } 150 | 151 | // Non-terminating context. 152 | nonTerm := context.Background() 153 | 154 | for i := 0; i < g.Concurrency; i++ { 155 | go func(i int) { 156 | rng := rand.New(rand.NewSource(int64(i))) 157 | rcv := c.Receiver() 158 | defer wg.Done() 159 | done := ctx.Done() 160 | var opts minio.PutObjectRetentionOptions 161 | 162 | <-wait 163 | mode := minio.Governance 164 | for { 165 | select { 166 | case <-done: 167 | return 168 | default: 169 | } 170 | 171 | if g.rpsLimit(ctx) != nil { 172 | return 173 | } 174 | 175 | obj := g.objects[rng.Intn(len(g.objects))] 176 | client, cldone := g.Client() 177 | op := Operation{ 178 | OpType: "RETENTION", 179 | Thread: uint16(i), 180 | Size: 0, 181 | File: obj.Name, 182 | ObjPerOp: 1, 183 | Endpoint: client.EndpointURL().String(), 184 | } 185 | 186 | op.Start = time.Now() 187 | opts.VersionID = obj.VersionID 188 | t := op.Start.Add(24 * time.Hour) 189 | opts.RetainUntilDate = &t 190 | opts.Mode = &mode 191 | opts.GovernanceBypass = true 192 | err := client.PutObjectRetention(nonTerm, g.Bucket, obj.Name, opts) 193 | if err != nil { 194 | g.Error("put retention error:", err) 195 | op.Err = err.Error() 196 | op.End = time.Now() 197 | rcv <- op 198 | cldone() 199 | continue 200 | } 201 | op.End = time.Now() 202 | rcv <- op 203 | cldone() 204 | } 205 | }(i) 206 | } 207 | wg.Wait() 208 | return nil 209 | } 210 | 211 | // Cleanup deletes everything uploaded to the bucket. 212 | func (g *Retention) Cleanup(ctx context.Context) { 213 | g.deleteAllInBucket(ctx, g.objects.Prefixes()...) 214 | } 215 | -------------------------------------------------------------------------------- /pkg/bench/s3zip.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2022 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "archive/zip" 22 | "context" 23 | "fmt" 24 | "io" 25 | "math/rand" 26 | "net/http" 27 | "path" 28 | "sync" 29 | "time" 30 | 31 | "github.com/minio/minio-go/v7" 32 | "github.com/minio/warp/pkg/generator" 33 | ) 34 | 35 | // S3Zip benchmarks download from a zip file. 36 | type S3Zip struct { 37 | Common 38 | ZipObjName string 39 | objects generator.Objects 40 | 41 | CreateFiles int 42 | } 43 | 44 | // Prepare will create an empty bucket or delete any content already there 45 | // and upload a number of objects. 46 | func (g *S3Zip) Prepare(ctx context.Context) error { 47 | if err := g.createEmptyBucket(ctx); err != nil { 48 | return err 49 | } 50 | 51 | src := g.Source() 52 | g.UpdateStatus(fmt.Sprint("Uploading", g.ZipObjName, "with ", g.CreateFiles, " files each of ", src.String())) 53 | 54 | client, cldone := g.Client() 55 | defer cldone() 56 | pr, pw := io.Pipe() 57 | zw := zip.NewWriter(pw) 58 | 59 | go func() { 60 | for i := 0; i < g.CreateFiles; i++ { 61 | opts := g.PutOpts 62 | done := ctx.Done() 63 | 64 | select { 65 | case <-done: 66 | return 67 | default: 68 | } 69 | 70 | obj := src.Object() 71 | 72 | opts.ContentType = obj.ContentType 73 | header := zip.FileHeader{ 74 | Name: obj.Name, 75 | Method: 0, 76 | } 77 | 78 | f, err := zw.CreateHeader(&header) 79 | if err != nil { 80 | err := fmt.Errorf("zip create error: %w", err) 81 | g.Error(err) 82 | pw.CloseWithError(err) 83 | return 84 | } 85 | _, err = io.Copy(f, obj.Reader) 86 | if err != nil { 87 | err := fmt.Errorf("zip write error: %w", err) 88 | g.Error(err) 89 | pw.CloseWithError(err) 90 | return 91 | } 92 | 93 | obj.Reader = nil 94 | g.objects = append(g.objects, *obj) 95 | g.prepareProgress(float64(i) / float64(g.CreateFiles)) 96 | } 97 | pw.CloseWithError(zw.Close()) 98 | }() 99 | 100 | // TODO: Add header to index. 101 | // g.PutOpts.Set("x-minio-extract", "true") 102 | _, err := client.PutObject(ctx, g.Bucket, g.ZipObjName, pr, -1, g.PutOpts) 103 | pr.CloseWithError(err) 104 | if err == nil { 105 | var opts minio.GetObjectOptions 106 | opts.Set("x-minio-extract", "true") 107 | 108 | oi, err2 := client.GetObject(ctx, g.Bucket, path.Join(g.ZipObjName, g.objects[0].Name), opts) 109 | if err2 != nil { 110 | err = err2 111 | } 112 | if err == nil { 113 | st, err2 := oi.Stat() 114 | err = err2 115 | if err == nil && st.Size != g.objects[0].Size { 116 | err = fmt.Errorf("unexpected download size. want: %v, got %v", g.objects[0].Size, st.Size) 117 | } 118 | } 119 | } 120 | return err 121 | } 122 | 123 | // Start will execute the main benchmark. 124 | // Operations should begin executing when the start channel is closed. 125 | func (g *S3Zip) Start(ctx context.Context, wait chan struct{}) error { 126 | var wg sync.WaitGroup 127 | wg.Add(g.Concurrency) 128 | c := g.Collector 129 | if g.AutoTermDur > 0 { 130 | ctx = c.AutoTerm(ctx, http.MethodGet, g.AutoTermScale, autoTermCheck, autoTermSamples, g.AutoTermDur) 131 | } 132 | 133 | // Non-terminating context. 134 | nonTerm := context.Background() 135 | 136 | for i := 0; i < g.Concurrency; i++ { 137 | go func(i int) { 138 | rng := rand.New(rand.NewSource(int64(i))) 139 | rcv := c.Receiver() 140 | defer wg.Done() 141 | done := ctx.Done() 142 | var opts minio.GetObjectOptions 143 | 144 | <-wait 145 | for { 146 | select { 147 | case <-done: 148 | return 149 | default: 150 | } 151 | 152 | if g.rpsLimit(ctx) != nil { 153 | return 154 | } 155 | 156 | fbr := firstByteRecorder{} 157 | obj := g.objects[rng.Intn(len(g.objects))] 158 | client, cldone := g.Client() 159 | op := Operation{ 160 | OpType: "GET", 161 | Thread: uint16(i), 162 | Size: obj.Size, 163 | File: path.Join(g.ZipObjName, obj.Name), 164 | ObjPerOp: 1, 165 | Endpoint: client.EndpointURL().String(), 166 | } 167 | 168 | op.Start = time.Now() 169 | opts.Set("x-minio-extract", "true") 170 | 171 | o, err := client.GetObject(nonTerm, g.Bucket, op.File, opts) 172 | if err != nil { 173 | g.Error("download error:", err) 174 | op.Err = err.Error() 175 | op.End = time.Now() 176 | rcv <- op 177 | cldone() 178 | continue 179 | } 180 | fbr.r = o 181 | n, err := io.Copy(io.Discard, &fbr) 182 | if err != nil { 183 | g.Error("download error:", err) 184 | op.Err = err.Error() 185 | } 186 | op.FirstByte = fbr.t 187 | op.End = time.Now() 188 | if n != op.Size && op.Err == "" { 189 | op.Err = fmt.Sprint("unexpected download size. want:", op.Size, ", got:", n) 190 | g.Error(op.Err) 191 | } 192 | rcv <- op 193 | cldone() 194 | o.Close() 195 | } 196 | }(i) 197 | } 198 | wg.Wait() 199 | return nil 200 | } 201 | 202 | // Cleanup deletes everything uploaded to the bucket. 203 | func (g *S3Zip) Cleanup(ctx context.Context) { 204 | g.deleteAllInBucket(ctx) 205 | } 206 | -------------------------------------------------------------------------------- /pkg/bench/snowball.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2023 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package bench 19 | 20 | import ( 21 | "archive/tar" 22 | "bytes" 23 | "context" 24 | "fmt" 25 | "io" 26 | "net/http" 27 | "os" 28 | "path" 29 | "sync" 30 | "time" 31 | 32 | "github.com/klauspost/compress/zstd" 33 | ) 34 | 35 | // Snowball benchmarks snowball upload speed. 36 | type Snowball struct { 37 | Common 38 | prefixes map[string]struct{} 39 | 40 | enc []*zstd.Encoder 41 | NumObjs int // Number objects in each snowball. 42 | WindowSize int 43 | Duplicate bool // Duplicate object content. 44 | Compress bool // Zstandard compress snowball. 45 | } 46 | 47 | // Prepare will create an empty bucket or delete any content already there 48 | // and upload a number of objects. 49 | func (s *Snowball) Prepare(ctx context.Context) error { 50 | if s.Compress { 51 | s.enc = make([]*zstd.Encoder, s.Concurrency) 52 | for i := range s.enc { 53 | var err error 54 | s.enc[i], err = zstd.NewWriter(nil, zstd.WithEncoderConcurrency(1), zstd.WithEncoderLevel(zstd.SpeedFastest), zstd.WithWindowSize(s.WindowSize), zstd.WithNoEntropyCompression(true), zstd.WithAllLitEntropyCompression(false)) 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | } 60 | s.prefixes = make(map[string]struct{}, s.Concurrency) 61 | return s.createEmptyBucket(ctx) 62 | } 63 | 64 | // Start will execute the main benchmark. 65 | // Operations should begin executing when the start channel is closed. 66 | func (s *Snowball) Start(ctx context.Context, wait chan struct{}) error { 67 | var wg sync.WaitGroup 68 | wg.Add(s.Concurrency) 69 | c := s.Collector 70 | if s.AutoTermDur > 0 { 71 | ctx = c.AutoTerm(ctx, http.MethodPut, s.AutoTermScale, autoTermCheck, autoTermSamples, s.AutoTermDur) 72 | } 73 | s.prefixes = make(map[string]struct{}, s.Concurrency) 74 | 75 | // Non-terminating context. 76 | nonTerm := context.Background() 77 | 78 | for i := 0; i < s.Concurrency; i++ { 79 | src := s.Source() 80 | s.prefixes[src.Prefix()] = struct{}{} 81 | go func(i int) { 82 | var buf bytes.Buffer 83 | rcv := c.Receiver() 84 | defer wg.Done() 85 | opts := s.PutOpts 86 | opts.UserMetadata = map[string]string{"X-Amz-Meta-Snowball-Auto-Extract": "true"} 87 | done := ctx.Done() 88 | 89 | <-wait 90 | for { 91 | select { 92 | case <-done: 93 | return 94 | default: 95 | } 96 | 97 | if s.rpsLimit(ctx) != nil { 98 | return 99 | } 100 | 101 | buf.Reset() 102 | w := io.Writer(&buf) 103 | if s.Compress { 104 | s.enc[i].Reset(&buf) 105 | w = s.enc[i] 106 | } 107 | obj := src.Object() 108 | op := Operation{ 109 | OpType: http.MethodPut, 110 | Thread: uint16(i), 111 | File: path.Join(obj.Prefix, "snowball.tar"), 112 | ObjPerOp: s.NumObjs, 113 | } 114 | 115 | { 116 | tw := tar.NewWriter(w) 117 | content, err := io.ReadAll(obj.Reader) 118 | if err != nil { 119 | s.Error("obj data error: ", err) 120 | return 121 | } 122 | 123 | for i := 0; i < s.NumObjs; i++ { 124 | err := tw.WriteHeader(&tar.Header{ 125 | Typeflag: tar.TypeReg, 126 | Name: fmt.Sprintf("%s/%d.obj", obj.Name, i), 127 | Size: obj.Size, 128 | Mode: int64(os.ModePerm), 129 | ModTime: time.Now(), 130 | }) 131 | if err != nil { 132 | s.Error("tar header error: ", err) 133 | return 134 | } 135 | _, err = tw.Write(content) 136 | if err != nil { 137 | s.Error("tar write error: ", err) 138 | return 139 | } 140 | op.Size += int64(len(content)) 141 | if !s.Duplicate { 142 | obj = src.Object() 143 | content, err = io.ReadAll(obj.Reader) 144 | if err != nil { 145 | s.Error("obj data error: ", err) 146 | return 147 | } 148 | } 149 | } 150 | tw.Close() 151 | if s.Compress { 152 | err := s.enc[i].Close() 153 | if err != nil { 154 | s.Error("zstd close error: ", err) 155 | return 156 | } 157 | } 158 | } 159 | opts.ContentType = obj.ContentType 160 | opts.DisableMultipart = true 161 | 162 | client, cldone := s.Client() 163 | op.Endpoint = client.EndpointURL().String() 164 | op.Start = time.Now() 165 | tarLength := int64(buf.Len()) 166 | // fmt.Println(op.Size, "->", tarLength, math.Round(100*float64(tarLength)/float64(op.Size)), "%") 167 | res, err := client.PutObject(nonTerm, s.Bucket, obj.Name+".tar", &buf, tarLength, opts) 168 | op.End = time.Now() 169 | if err != nil { 170 | s.Error("upload error: ", err) 171 | op.Err = err.Error() 172 | } 173 | obj.VersionID = res.VersionID 174 | 175 | if res.Size != tarLength && op.Err == "" { 176 | err := fmt.Sprint("short upload. want:", tarLength, ", got:", res.Size) 177 | if op.Err == "" { 178 | op.Err = err 179 | } 180 | s.Error(err) 181 | } 182 | cldone() 183 | rcv <- op 184 | } 185 | }(i) 186 | } 187 | wg.Wait() 188 | return nil 189 | } 190 | 191 | // Cleanup deletes everything uploaded to the bucket. 192 | func (s *Snowball) Cleanup(ctx context.Context) { 193 | if s.Compress { 194 | for i := range s.enc { 195 | s.enc[i] = nil 196 | } 197 | } 198 | pf := make([]string, 0, len(s.prefixes)) 199 | for p := range s.prefixes { 200 | pf = append(pf, p) 201 | } 202 | s.deleteAllInBucket(ctx, pf...) 203 | } 204 | -------------------------------------------------------------------------------- /pkg/bench/testdata/warp-benchdata-get.csv.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minio/warp/4736827d64c59b604f64fb332a062da769150441/pkg/bench/testdata/warp-benchdata-get.csv.zst -------------------------------------------------------------------------------- /pkg/build-constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package pkg 19 | 20 | var ( 21 | // Version - the version being released (v prefix stripped) 22 | Version = "(dev)" 23 | // ReleaseTag - the current git tag 24 | ReleaseTag = "(no tag)" 25 | // ReleaseTime - current UTC date in RFC3339 format. 26 | ReleaseTime = "(no release)" 27 | // CommitID - latest commit id. 28 | CommitID = "(dev)" 29 | // ShortCommitID - first 12 characters from CommitID. 30 | ShortCommitID = "(dev)" 31 | ) 32 | -------------------------------------------------------------------------------- /pkg/generator/generator.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package generator 19 | 20 | import ( 21 | "errors" 22 | "io" 23 | "math" 24 | "math/rand" 25 | "path" 26 | "runtime" 27 | ) 28 | 29 | // Option provides options for data generation. 30 | // Use WithXXXX().Apply() to select data types and set options. 31 | type Option func(o *Options) error 32 | 33 | type Source interface { 34 | // Requesting a new reader will scramble data, so the new reader will not return the same data. 35 | // Requesting a reader is designed to be as lightweight as possible. 36 | // Only a single reader can be used concurrently. 37 | Object() *Object 38 | 39 | // String returns a human readable description of the source. 40 | String() string 41 | 42 | // Prefix returns the prefix if any. 43 | Prefix() string 44 | } 45 | 46 | type Object struct { 47 | // Reader will return a reader that will return the number of requested bytes 48 | // and EOF on all subsequent calls. 49 | Reader io.ReadSeeker 50 | 51 | // A random generated name. 52 | Name string 53 | 54 | // Corresponding mime type 55 | ContentType string 56 | 57 | Prefix string 58 | 59 | VersionID string 60 | 61 | // Size of the object to expect. 62 | Size int64 63 | } 64 | 65 | // Objects is a slice of objects. 66 | type Objects []Object 67 | 68 | // Prefixes returns all prefixes. 69 | func (o Objects) Prefixes() []string { 70 | prefixes := make(map[string]struct{}, runtime.GOMAXPROCS(0)) 71 | for _, p := range o { 72 | prefixes[p.Prefix] = struct{}{} 73 | } 74 | res := make([]string, 0, len(prefixes)) 75 | for p := range prefixes { 76 | res = append(res, p) 77 | } 78 | return res 79 | } 80 | 81 | // MergeObjectPrefixes merges prefixes from several slices of objects. 82 | func MergeObjectPrefixes(o []Objects) []string { 83 | prefixes := make(map[string]struct{}, runtime.GOMAXPROCS(0)) 84 | for _, objs := range o { 85 | for _, p := range objs { 86 | prefixes[p.Prefix] = struct{}{} 87 | } 88 | } 89 | res := make([]string, 0, len(prefixes)) 90 | for p := range prefixes { 91 | res = append(res, p) 92 | } 93 | return res 94 | } 95 | 96 | func (o *Object) setPrefix(opts Options) { 97 | if opts.randomPrefix <= 0 { 98 | o.Prefix = opts.customPrefix 99 | return 100 | } 101 | b := make([]byte, opts.randomPrefix) 102 | rng := rand.New(rand.NewSource(int64(rand.Uint64()))) 103 | randASCIIBytes(b, rng) 104 | o.Prefix = path.Join(opts.customPrefix, string(b)) 105 | } 106 | 107 | func (o *Object) setName(s string) { 108 | if len(o.Prefix) == 0 { 109 | o.Name = s 110 | return 111 | } 112 | o.Name = o.Prefix + "/" + s 113 | } 114 | 115 | // New return data source. 116 | func New(opts ...Option) (Source, error) { 117 | options := defaultOptions() 118 | for _, ofn := range opts { 119 | err := ofn(&options) 120 | if err != nil { 121 | return nil, err 122 | } 123 | } 124 | if options.src == nil { 125 | return nil, errors.New("internal error: generator Source was nil") 126 | } 127 | return options.src(options) 128 | } 129 | 130 | // NewFn return data source. 131 | func NewFn(opts ...Option) (func() Source, error) { 132 | options := defaultOptions() 133 | for _, ofn := range opts { 134 | err := ofn(&options) 135 | if err != nil { 136 | return nil, err 137 | } 138 | } 139 | if options.src == nil { 140 | return nil, errors.New("internal error: generator Source was nil") 141 | } 142 | 143 | return func() Source { 144 | s, err := options.src(options) 145 | if err != nil { 146 | panic(err) 147 | } 148 | return s 149 | }, nil 150 | } 151 | 152 | const asciiLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890()" 153 | 154 | var asciiLetterBytes [len(asciiLetters)]byte 155 | 156 | func init() { 157 | for i, v := range asciiLetters { 158 | asciiLetterBytes[i] = byte(v) 159 | } 160 | } 161 | 162 | // randASCIIBytes fill destination with pseudorandom ASCII characters [a-ZA-Z0-9]. 163 | // Should never be considered for true random data generation. 164 | func randASCIIBytes(dst []byte, rng *rand.Rand) { 165 | // Use a single seed. 166 | v := rng.Uint64() 167 | rnd := uint32(v) 168 | rnd2 := uint32(v >> 32) 169 | for i := range dst { 170 | dst[i] = asciiLetterBytes[int(rnd>>16)%len(asciiLetterBytes)] 171 | rnd ^= rnd2 172 | rnd *= 2654435761 173 | } 174 | } 175 | 176 | // GetExpRandSize will return an exponential random size from 1 to and including max. 177 | // Minimum size: 127 bytes, max scale is 256 times smaller than max size. 178 | // Average size will be max_size * 0.179151. 179 | func GetExpRandSize(rng *rand.Rand, minSize, maxSize int64) int64 { 180 | if maxSize-minSize < 10 { 181 | if maxSize-minSize <= 0 { 182 | return 0 183 | } 184 | return 1 + minSize + rng.Int63n(maxSize-minSize) 185 | } 186 | logSizeMaxSize := math.Log2(float64(maxSize - 1)) 187 | logSizeMinSize := math.Max(7, logSizeMaxSize-8) 188 | if minSize > 0 { 189 | logSizeMinSize = math.Log2(float64(minSize - 1)) 190 | } 191 | lsDelta := logSizeMaxSize - logSizeMinSize 192 | random := rng.Float64() 193 | logSize := random * lsDelta 194 | if logSize > 1 { 195 | return 1 + int64(math.Pow(2, logSize+logSizeMinSize)) 196 | } 197 | // For lowest part, do equal distribution 198 | return 1 + minSize + int64(random*math.Pow(2, logSizeMinSize+1)) 199 | } 200 | -------------------------------------------------------------------------------- /pkg/generator/generator_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package generator 19 | 20 | import ( 21 | "io" 22 | "io/ioutil" 23 | "testing" 24 | ) 25 | 26 | func TestNew(t *testing.T) { 27 | type args struct { 28 | opts []Option 29 | } 30 | tests := []struct { 31 | name string 32 | args args 33 | wantErr bool 34 | wantSize int 35 | }{ 36 | { 37 | name: "Default", 38 | args: args{ 39 | opts: nil, 40 | }, 41 | wantErr: false, 42 | wantSize: 1 << 20, 43 | }, 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | got, err := New(tt.args.opts...) 48 | if (err != nil) != tt.wantErr { 49 | t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) 50 | return 51 | } 52 | if err != nil { 53 | t.Error(err) 54 | return 55 | } 56 | if got == nil { 57 | t.Errorf("New() got = nil, want not nil") 58 | return 59 | } 60 | obj := got.Object() 61 | b, err := io.ReadAll(obj.Reader) 62 | if err != nil { 63 | t.Error(err) 64 | return 65 | } 66 | if len(b) != tt.wantSize { 67 | t.Errorf("New() size = %v, wantSize = %v", len(b), tt.wantSize) 68 | return 69 | } 70 | n, err := obj.Reader.Seek(0, 0) 71 | if err != nil { 72 | t.Error(err) 73 | return 74 | } 75 | if n != 0 { 76 | t.Errorf("Expected 0, got %v", n) 77 | return 78 | } 79 | b, err = ioutil.ReadAll(obj.Reader) 80 | if err != nil { 81 | t.Error(err) 82 | return 83 | } 84 | if len(b) != tt.wantSize { 85 | t.Errorf("New() size = %v, wantSize = %v", len(b), tt.wantSize) 86 | return 87 | } 88 | n, err = obj.Reader.Seek(10, 0) 89 | if err != nil { 90 | t.Error(err) 91 | return 92 | } 93 | if n != 10 { 94 | t.Errorf("Expected 10, got %v", n) 95 | return 96 | } 97 | b, err = io.ReadAll(obj.Reader) 98 | if err != nil { 99 | return 100 | } 101 | if len(b) != tt.wantSize-10 { 102 | t.Errorf("New() size = %v, wantSize = %v", len(b), tt.wantSize) 103 | return 104 | } 105 | n, err = obj.Reader.Seek(10, io.SeekCurrent) 106 | if err != io.ErrUnexpectedEOF { 107 | t.Errorf("Expected io.ErrUnexpectedEOF, got %v", err) 108 | return 109 | } 110 | if n != 0 { 111 | t.Errorf("Expected 0, got %v", n) 112 | return 113 | } 114 | }) 115 | } 116 | } 117 | 118 | func BenchmarkWithRandomData(b *testing.B) { 119 | type args struct { 120 | opts []Option 121 | } 122 | tests := []struct { 123 | name string 124 | args args 125 | }{ 126 | { 127 | name: "64KiB", 128 | args: args{opts: []Option{WithSize(1 << 16), WithRandomData().Apply()}}, 129 | }, 130 | { 131 | name: "1MiB", 132 | args: args{opts: []Option{WithSize(1 << 20), WithRandomData().Apply()}}, 133 | }, 134 | { 135 | name: "10MiB", 136 | args: args{opts: []Option{WithSize(10 << 20), WithRandomData().Apply()}}, 137 | }, 138 | { 139 | name: "10GiB", 140 | args: args{opts: []Option{WithSize(10 << 30), WithRandomData().Apply()}}, 141 | }, 142 | } 143 | for _, tt := range tests { 144 | b.Run(tt.name, func(b *testing.B) { 145 | got, err := New(tt.args.opts...) 146 | if err != nil { 147 | b.Errorf("New() error = %v", err) 148 | return 149 | } 150 | obj := got.Object() 151 | n, err := io.Copy(io.Discard, obj.Reader) 152 | if err != nil { 153 | b.Errorf("ioutil error = %v", err) 154 | return 155 | } 156 | b.SetBytes(n) 157 | b.ReportAllocs() 158 | b.ResetTimer() 159 | for i := 0; i < b.N; i++ { 160 | obj = got.Object() 161 | _, err := io.Copy(io.Discard, obj.Reader) 162 | if err != nil { 163 | b.Errorf("New() error = %v", err) 164 | return 165 | } 166 | } 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /pkg/generator/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package generator 19 | 20 | import ( 21 | "errors" 22 | "math/rand" 23 | 24 | hist "github.com/jfsmig/prng/histogram" 25 | ) 26 | 27 | // Options provides options. 28 | // Use WithXXX functions to set them. 29 | type Options struct { 30 | src func(o Options) (Source, error) 31 | customPrefix string 32 | random RandomOpts 33 | minSize int64 34 | totalSize int64 35 | randomPrefix int 36 | randSize bool 37 | 38 | // Activates the use of a distribution of sizes 39 | flagSizesDistribution bool 40 | sizesDistribution hist.Int64Distribution 41 | } 42 | 43 | // OptionApplier allows to abstract generator options. 44 | type OptionApplier interface { 45 | Apply() Option 46 | } 47 | 48 | // getSize will return a size for an object. 49 | func (o Options) getSize(rng *rand.Rand) int64 { 50 | if o.flagSizesDistribution { 51 | return o.sizesDistribution.Poll(rng) 52 | } 53 | if !o.randSize { 54 | return o.totalSize 55 | } 56 | return GetExpRandSize(rng, o.minSize, o.totalSize) 57 | } 58 | 59 | func defaultOptions() Options { 60 | o := Options{ 61 | src: newRandom, 62 | totalSize: 1 << 20, 63 | random: randomOptsDefaults(), 64 | randomPrefix: 0, 65 | } 66 | return o 67 | } 68 | 69 | func WithSizeHistograms(encoded string) Option { 70 | return func(o *Options) error { 71 | var err error 72 | o.sizesDistribution, err = hist.ParseCSV(encoded) 73 | if err != nil { 74 | return err 75 | } 76 | o.flagSizesDistribution = true 77 | return nil 78 | } 79 | } 80 | 81 | // WithMinMaxSize sets the min and max size of the generated data. 82 | func WithMinMaxSize(minSize, maxSize int64) Option { 83 | return func(o *Options) error { 84 | if minSize <= 0 { 85 | return errors.New("WithMinMaxSize: minSize must be >= 0") 86 | } 87 | if maxSize < 0 { 88 | return errors.New("WithMinMaxSize: maxSize must be > 0") 89 | } 90 | if minSize > maxSize { 91 | return errors.New("WithMinMaxSize: minSize must be < maxSize") 92 | } 93 | if o.randSize && maxSize < 256 { 94 | return errors.New("WithMinMaxSize: random sized objects should be at least 256 bytes") 95 | } 96 | 97 | o.totalSize = maxSize 98 | o.minSize = minSize 99 | return nil 100 | } 101 | } 102 | 103 | // WithSize sets the size of the generated data. 104 | func WithSize(n int64) Option { 105 | return func(o *Options) error { 106 | if n <= 0 { 107 | return errors.New("WithSize: size must be > 0") 108 | } 109 | if o.randSize && o.totalSize < 256 { 110 | return errors.New("WithSize: random sized objects should be at least 256 bytes") 111 | } 112 | 113 | o.totalSize = n 114 | return nil 115 | } 116 | } 117 | 118 | // WithRandomSize will randomize the size from 1 byte to the total size set. 119 | func WithRandomSize(b bool) Option { 120 | return func(o *Options) error { 121 | if b && o.totalSize > 0 && o.totalSize < 256 { 122 | return errors.New("WithRandomSize: Random sized objects should be at least 256 bytes") 123 | } 124 | o.randSize = b 125 | return nil 126 | } 127 | } 128 | 129 | // WithCustomPrefix adds custom prefix under bucket where all warp content is created. 130 | func WithCustomPrefix(prefix string) Option { 131 | return func(o *Options) error { 132 | o.customPrefix = prefix 133 | return nil 134 | } 135 | } 136 | 137 | // WithPrefixSize sets prefix size. 138 | func WithPrefixSize(n int) Option { 139 | return func(o *Options) error { 140 | if n < 0 { 141 | return errors.New("WithPrefixSize: size must be >= 0 and <= 16") 142 | } 143 | if n > 16 { 144 | return errors.New("WithPrefixSize: size must be >= 0 and <= 16") 145 | } 146 | o.randomPrefix = n 147 | return nil 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /pkg/generator/random.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Warp (C) 2019-2020 MinIO, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package generator 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "math/rand" 24 | "sync/atomic" 25 | 26 | "github.com/minio/pkg/v3/rng" 27 | ) 28 | 29 | func WithRandomData() RandomOpts { 30 | return randomOptsDefaults() 31 | } 32 | 33 | // Apply Random data options. 34 | func (o RandomOpts) Apply() Option { 35 | return func(opts *Options) error { 36 | if err := o.validate(); err != nil { 37 | return err 38 | } 39 | opts.random = o 40 | opts.src = newRandom 41 | return nil 42 | } 43 | } 44 | 45 | func (o RandomOpts) validate() error { 46 | if o.size <= 0 { 47 | return errors.New("random: size <= 0") 48 | } 49 | return nil 50 | } 51 | 52 | // RngSeed will which to a fixed RNG seed to make usage predictable. 53 | func (o RandomOpts) RngSeed(s int64) RandomOpts { 54 | o.seed = &s 55 | return o 56 | } 57 | 58 | // Size will set a block size. 59 | // Data of this size will be repeated until output size has been reached. 60 | func (o RandomOpts) Size(s int) RandomOpts { 61 | o.size = s 62 | return o 63 | } 64 | 65 | // RandomOpts are the options for the random data source. 66 | type RandomOpts struct { 67 | seed *int64 68 | size int 69 | } 70 | 71 | func randomOptsDefaults() RandomOpts { 72 | return RandomOpts{ 73 | seed: nil, 74 | // Use 128KB as base. 75 | size: 128 << 10, 76 | } 77 | } 78 | 79 | type randomSrc struct { 80 | source *rng.Reader 81 | rng *rand.Rand 82 | obj Object 83 | o Options 84 | counter uint64 85 | } 86 | 87 | func newRandom(o Options) (Source, error) { 88 | rndSrc := rand.NewSource(int64(rand.Uint64())) 89 | if o.random.seed != nil { 90 | rndSrc = rand.NewSource(*o.random.seed) 91 | } 92 | 93 | size := o.random.size 94 | if int64(size) > o.totalSize { 95 | size = int(o.totalSize) 96 | } 97 | if size <= 0 { 98 | return nil, fmt.Errorf("size must be >= 0, got %d", size) 99 | } 100 | 101 | input, err := rng.NewReader(rng.WithRNG(rand.New(rndSrc)), rng.WithSize(o.totalSize)) 102 | if err != nil { 103 | return nil, err 104 | } 105 | r := randomSrc{ 106 | o: o, 107 | rng: rand.New(rndSrc), 108 | source: input, 109 | obj: Object{ 110 | Reader: nil, 111 | Name: "", 112 | ContentType: "application/octet-stream", 113 | Size: 0, 114 | }, 115 | } 116 | r.obj.setPrefix(o) 117 | return &r, nil 118 | } 119 | 120 | func (r *randomSrc) Object() *Object { 121 | atomic.AddUint64(&r.counter, 1) 122 | var nBuf [16]byte 123 | randASCIIBytes(nBuf[:], r.rng) 124 | r.obj.Size = r.o.getSize(r.rng) 125 | r.obj.setName(fmt.Sprintf("%d.%s.rnd", atomic.LoadUint64(&r.counter), string(nBuf[:]))) 126 | 127 | // Reset scrambler 128 | r.source.ResetSize(r.obj.Size) 129 | r.obj.Reader = r.source 130 | return &r.obj 131 | } 132 | 133 | func (r *randomSrc) String() string { 134 | if r.o.randSize { 135 | return fmt.Sprintf("Random data; random size up to %d bytes", r.o.totalSize) 136 | } 137 | return fmt.Sprintf("Random data; %d bytes total", r.o.totalSize) 138 | } 139 | 140 | func (r *randomSrc) Prefix() string { 141 | return r.obj.Prefix 142 | } 143 | -------------------------------------------------------------------------------- /systemd/warp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Warp 3 | Documentation=https://github.com/minio/warp 4 | Wants=network-online.target 5 | After=network-online.target 6 | AssertFileIsExecutable=/usr/bin/warp 7 | 8 | [Service] 9 | WorkingDirectory=/tmp 10 | 11 | ExecStart=/usr/bin/warp client 12 | 13 | # Let systemd restart this service always 14 | Restart=always 15 | 16 | # Specifies the maximum file descriptor number that can be opened by this process 17 | LimitNOFILE=65536 18 | 19 | # Disable timeout logic and wait until process is stopped 20 | TimeoutStopSec=infinity 21 | SendSIGKILL=no 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | 26 | # Built for ${project.name}-${project.version} (${project.name}) -------------------------------------------------------------------------------- /warp_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minio/warp/4736827d64c59b604f64fb332a062da769150441/warp_logo.png -------------------------------------------------------------------------------- /yml-samples/mixed.yml: -------------------------------------------------------------------------------- 1 | warp: 2 | api: v1 3 | 4 | # Benchmark to run. 5 | # Corresponds to warp [benchmark] command. 6 | benchmark: mixed 7 | 8 | # Do not print any output. 9 | quiet: false 10 | 11 | # Disable terminal color output. 12 | no-color: false 13 | 14 | # Print results and errors as JSON. 15 | json: false 16 | 17 | # Output benchmark+profile data to this file. 18 | # By default a unique filename is generated. 19 | bench-data: 20 | 21 | # Connect to warp clients and run benchmarks there. 22 | # See https://github.com/minio/warp?tab=readme-ov-file#distributed-benchmarking 23 | # Can be a single value or a list. 24 | warp-client: 25 | 26 | # Run MinIO server profiling during benchmark; 27 | # possible values are 'cpu', 'cpuio', 'mem', 'block', 'mutex', 'threads' and 'trace'. 28 | # Can be single value or a list. 29 | server-profile: 30 | 31 | # Remote host parameters and connection info. 32 | remote: 33 | # Specify custom region 34 | region: us-east-1 35 | 36 | # Access key and Secret key 37 | access-key: 'Q3AM3UQ867SPQQA43P2F' 38 | secret-key: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' 39 | 40 | # Specify one or more hosts. 41 | # The benchmark will be run against all hosts concurrently. 42 | # Multiple servers can be specified with ellipsis notation; 43 | # for example '10.0.0.{1...10}:9000' specifies 10 hosts. 44 | # See more at https://github.com/minio/warp?tab=readme-ov-file#multiple-hosts 45 | host: 46 | - 'play.min.io' 47 | 48 | # Use TLS for calls. 49 | tls: true 50 | 51 | # Allow TLS with unverified certificates. 52 | insecure: false 53 | 54 | # Stream benchmark statistics to Influx DB instance. 55 | # See more at https://github.com/minio/warp?tab=readme-ov-file#influxdb-output 56 | influxdb: '' 57 | 58 | # Bucket to use for benchmark data. 59 | # 60 | # CAREFUL: ALL DATA WILL BE DELETED IN BUCKET! 61 | # 62 | # By default, 'warp-benchmark-bucket' will be created or used. 63 | bucket: 64 | 65 | # params specifies the benchmark parameters. 66 | # The fields here depend on the benchmark type. 67 | params: 68 | # Duration to run the benchmark. 69 | # Use 's' and 'm' to specify seconds and minutes. 70 | duration: 1m 71 | 72 | # Concurrent operations to run per warp instance. 73 | concurrent: 8 74 | 75 | # The number of objects to upload before starting the benchmark. 76 | # Upload enough objects to ensure that any remote caching is bypassed. 77 | objects: 1000 78 | 79 | # Adjust the distribution of each operation type 80 | # The final distribution will be determined by the fraction of each value of the total. 81 | distribution: 82 | get: 45.0 83 | stat: 30.0 84 | put: 15.0 85 | delete: 10.0 # Must be same or lower than 'put'. 86 | 87 | # Properties of uploaded objects. 88 | obj: 89 | # Size of each uploaded object 90 | size: 100KiB 91 | 92 | # Randomize the size of each object within certain constraints. 93 | # See https://github.com/minio/warp?tab=readme-ov-file#random-file-sizes 94 | rand-size: false 95 | 96 | # Force specific size of each multipart part. 97 | # Must be '5MB' or bigger. 98 | part-size: 99 | 100 | # Use automatic termination when traffic stabilizes. 101 | # Can not be used with distributed warp setup. 102 | # See https://github.com/minio/warp?tab=readme-ov-file#automatic-termination 103 | autoterm: 104 | enabled: false 105 | dur: 10s 106 | pct: 7.5 107 | 108 | # Do not clear bucket before or after running benchmarks. 109 | no-clear: false 110 | 111 | # Leave benchmark data. Do not run cleanup after benchmark. 112 | # Bucket will still be cleaned prior to benchmark. 113 | keep-data: false 114 | 115 | 116 | # The io section specifies custom IO properties for uploaded objects. 117 | io: 118 | # Use a custom prefix 119 | prefix: 120 | 121 | # Do not use separate prefix for each thread 122 | no-prefix: false 123 | 124 | # Add MD5 sum to uploads 125 | md5: false 126 | 127 | # Disable multipart uploads 128 | disable-multipart: false 129 | 130 | # Disable calculating sha256 on client side for uploads 131 | disable-sha256-payload: false 132 | 133 | # Server-side sse-s3 encrypt/decrypt objects 134 | sse-s3-encrypt: false 135 | 136 | # Encrypt/decrypt objects (using server-side encryption with random keys) 137 | sse-c-encrypt: false 138 | 139 | # Override storage class. 140 | # Default storage class will be used unless specified. 141 | storage-class: 142 | 143 | analyze: 144 | # Display additional analysis data. 145 | verbose: false 146 | # Only output for this host. 147 | host: '' 148 | # Only output for this operation. Can be 'GET', 'PUT', 'DELETE', etc. 149 | filter-op: '' 150 | # Split analysis into durations of this length. 151 | # Can be '1s', '5s', '1m', etc. 152 | segment-duration: 153 | # Output aggregated data as to file. 154 | out: 155 | # Additional time duration to skip when analyzing data. 156 | skip-duration: 157 | # Max operations to load for analysis. 158 | limit: 159 | # Skip this number of operations before starting analysis. 160 | offset: 161 | 162 | advanced: 163 | # Stress test only and discard output. 164 | stress: false 165 | 166 | # Print requests. 167 | debug: false 168 | 169 | # Disable HTTP Keep-Alive 170 | disable-http-keepalive: false 171 | 172 | # Enable HTTP2 support if server supports it 173 | http2: false 174 | 175 | # Rate limit each instance to this number of requests per second 176 | rps-limit: 177 | 178 | # Host selection algorithm. 179 | # Can be 'weighed' or 'roundrobin' 180 | host-select: weighed 181 | 182 | # "Resolve the host(s) ip(s) (including multiple A/AAAA records). 183 | # This can break SSL certificates, use --insecure if so 184 | resolve-host: false 185 | 186 | # Specify custom write socket buffer size in bytes 187 | sndbuf: 32768 188 | 189 | # Specify custom read socket buffer size in bytes 190 | rcvbuf: 32768 191 | 192 | # When running benchmarks open a webserver to fetch results remotely, eg: localhost:7762 193 | serve: 194 | -------------------------------------------------------------------------------- /yml-samples/multipart.yml: -------------------------------------------------------------------------------- 1 | warp: 2 | api: v1 3 | 4 | # Benchmark to run. 5 | # Corresponds to warp [benchmark] command. 6 | benchmark: multipart 7 | 8 | # Do not print any output. 9 | quiet: false 10 | 11 | # Disable terminal color output. 12 | no-color: false 13 | 14 | # Print results and errors as JSON. 15 | json: false 16 | 17 | # Output benchmark+profile data to this file. 18 | # By default a unique filename is generated. 19 | bench-data: 20 | 21 | # Connect to warp clients and run benchmarks there. 22 | # See https://github.com/minio/warp?tab=readme-ov-file#distributed-benchmarking 23 | # Can be a single value or a list. 24 | warp-client: 25 | 26 | # Run MinIO server profiling during benchmark; 27 | # possible values are 'cpu', 'cpuio', 'mem', 'block', 'mutex', 'threads' and 'trace'. 28 | # Can be single value or a list. 29 | server-profile: 30 | 31 | # Remote host parameters and connection info. 32 | remote: 33 | # Specify custom region 34 | region: us-east-1 35 | 36 | # Access key and Secret key 37 | access-key: 'Q3AM3UQ867SPQQA43P2F' 38 | secret-key: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' 39 | 40 | # Specify one or more hosts. 41 | # The benchmark will be run against all hosts concurrently. 42 | # Multiple servers can be specified with ellipsis notation; 43 | # for example '10.0.0.{1...10}:9000' specifies 10 hosts. 44 | # See more at https://github.com/minio/warp?tab=readme-ov-file#multiple-hosts 45 | host: 46 | - 'play.min.io' 47 | 48 | # Use TLS for calls. 49 | tls: true 50 | 51 | # Allow TLS with unverified certificates. 52 | insecure: false 53 | 54 | # Stream benchmark statistics to Influx DB instance. 55 | # See more at https://github.com/minio/warp?tab=readme-ov-file#influxdb-output 56 | influxdb: '' 57 | 58 | # Bucket to use for benchmark data. 59 | # 60 | # CAREFUL: ALL DATA WILL BE DELETED IN BUCKET! 61 | # 62 | # By default, 'warp-benchmark-bucket' will be created or used. 63 | bucket: 64 | 65 | # params specifies the benchmark parameters. 66 | # The fields here depend on the benchmark type. 67 | params: 68 | # Duration to run the benchmark. 69 | # Use 's' and 'm' to specify seconds and minutes. 70 | duration: 1m 71 | 72 | # Concurrent operations to run per warp instance. 73 | concurrent: 8 74 | 75 | # Properties of uploaded object. 76 | obj: 77 | # Object name 78 | name: 'warp-multipart.bin' 79 | 80 | # Parts to add per warp client 81 | parts: 16 82 | 83 | # Size of each multipart part. 84 | # Size of each part. Can be a number or MiB/GiB. 85 | # Must be greater than or equal to 5MiB. 86 | part-size: 5MiB 87 | 88 | # Use automatic termination when traffic stabilizes. 89 | # Can not be used with distributed warp setup. 90 | # See https://github.com/minio/warp?tab=readme-ov-file#automatic-termination 91 | autoterm: 92 | enabled: false 93 | dur: 10s 94 | pct: 7.5 95 | 96 | # Do not clear bucket before or after running benchmarks. 97 | no-clear: false 98 | 99 | # Leave benchmark data. Do not run cleanup after benchmark. 100 | # Bucket will still be cleaned prior to benchmark. 101 | keep-data: false 102 | 103 | 104 | # The io section specifies custom IO properties for uploaded objects. 105 | io: 106 | # Use a custom prefix 107 | prefix: 108 | 109 | # Do not use separate prefix for each thread 110 | no-prefix: false 111 | 112 | # Add MD5 sum to uploads 113 | md5: false 114 | 115 | # Disable multipart uploads 116 | disable-multipart: false 117 | 118 | # Disable calculating sha256 on client side for uploads 119 | disable-sha256-payload: false 120 | 121 | # Server-side sse-s3 encrypt/decrypt objects 122 | sse-s3-encrypt: false 123 | 124 | # Encrypt/decrypt objects (using server-side encryption with random keys) 125 | sse-c-encrypt: false 126 | 127 | # Override storage class. 128 | # Default storage class will be used unless specified. 129 | storage-class: 130 | 131 | analyze: 132 | # Display additional analysis data. 133 | verbose: false 134 | # Only output for this host. 135 | host: '' 136 | # Only output for this operation. Can be 'GET', 'PUT', 'DELETE', etc. 137 | filter-op: '' 138 | # Split analysis into durations of this length. 139 | # Can be '1s', '5s', '1m', etc. 140 | segment-duration: 141 | # Output aggregated data as to file. 142 | out: 143 | # Additional time duration to skip when analyzing data. 144 | skip-duration: 145 | # Max operations to load for analysis. 146 | limit: 147 | # Skip this number of operations before starting analysis. 148 | offset: 149 | 150 | advanced: 151 | # Stress test only and discard output. 152 | stress: false 153 | 154 | # Print requests. 155 | debug: false 156 | 157 | # Disable HTTP Keep-Alive 158 | disable-http-keepalive: false 159 | 160 | # Enable HTTP2 support if server supports it 161 | http2: false 162 | 163 | # Rate limit each instance to this number of requests per second 164 | rps-limit: 165 | 166 | # Host selection algorithm. 167 | # Can be 'weighed' or 'roundrobin' 168 | host-select: weighed 169 | 170 | # "Resolve the host(s) ip(s) (including multiple A/AAAA records). 171 | # This can break SSL certificates, use --insecure if so 172 | resolve-host: false 173 | 174 | # Specify custom write socket buffer size in bytes 175 | sndbuf: 32768 176 | 177 | # Specify custom read socket buffer size in bytes 178 | rcvbuf: 32768 179 | 180 | # When running benchmarks open a webserver to fetch results remotely, eg: localhost:7762 181 | serve: 182 | -------------------------------------------------------------------------------- /yml-samples/put.yml: -------------------------------------------------------------------------------- 1 | warp: 2 | api: v1 3 | 4 | # Benchmark to run. 5 | # Corresponds to warp [benchmark] command. 6 | benchmark: put 7 | 8 | # Do not print any output. 9 | quiet: false 10 | 11 | # Disable terminal color output. 12 | no-color: false 13 | 14 | # Print results and errors as JSON. 15 | json: false 16 | 17 | # Output benchmark+profile data to this file. 18 | # By default a unique filename is generated. 19 | bench-data: 20 | 21 | # Connect to warp clients and run benchmarks there. 22 | # See https://github.com/minio/warp?tab=readme-ov-file#distributed-benchmarking 23 | # Can be a single value or a list. 24 | warp-client: 25 | 26 | # Run MinIO server profiling during benchmark; 27 | # possible values are 'cpu', 'cpuio', 'mem', 'block', 'mutex', 'threads' and 'trace'. 28 | # Can be single value or a list. 29 | server-profile: 30 | 31 | # Remote host parameters and connection info. 32 | remote: 33 | # Specify custom region 34 | region: us-east-1 35 | 36 | # Access key and Secret key 37 | access-key: 'Q3AM3UQ867SPQQA43P2F' 38 | secret-key: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' 39 | 40 | # Specify one or more hosts. 41 | # The benchmark will be run against all hosts concurrently. 42 | # Multiple servers can be specified with ellipsis notation; 43 | # for example '10.0.0.{1...10}:9000' specifies 10 hosts. 44 | # See more at https://github.com/minio/warp?tab=readme-ov-file#multiple-hosts 45 | host: 46 | - 'play.min.io' 47 | 48 | # Use TLS for calls. 49 | tls: true 50 | 51 | # Allow TLS with unverified certificates. 52 | insecure: false 53 | 54 | # Stream benchmark statistics to Influx DB instance. 55 | # See more at https://github.com/minio/warp?tab=readme-ov-file#influxdb-output 56 | influxdb: '' 57 | 58 | # Bucket to use for benchmark data. 59 | # 60 | # CAREFUL: ALL DATA WILL BE DELETED IN BUCKET! 61 | # 62 | # By default, 'warp-benchmark-bucket' will be created or used. 63 | bucket: 64 | 65 | # params specifies the benchmark parameters. 66 | # The fields here depend on the benchmark type. 67 | params: 68 | # Duration to run the benchmark. 69 | # Use 's' and 'm' to specify seconds and minutes. 70 | duration: 1m 71 | 72 | # Concurrent operations to run per warp instance. 73 | concurrent: 8 74 | 75 | # Use POST Object operations for upload. 76 | post: false 77 | 78 | # Properties of uploaded objects. 79 | obj: 80 | # Size of each uploaded object 81 | size: 100KiB 82 | 83 | # Randomize the size of each object within certain constraints. 84 | # See https://github.com/minio/warp?tab=readme-ov-file#random-file-sizes 85 | rand-size: false 86 | 87 | # Force specific size of each multipart part. 88 | # Must be '5MB' or bigger. 89 | part-size: 90 | 91 | # Use automatic termination when traffic stabilizes. 92 | # Can not be used with distributed warp setup. 93 | # See https://github.com/minio/warp?tab=readme-ov-file#automatic-termination 94 | autoterm: 95 | enabled: false 96 | dur: 10s 97 | pct: 7.5 98 | 99 | # Do not clear bucket before or after running benchmarks. 100 | no-clear: false 101 | 102 | # Leave benchmark data. Do not run cleanup after benchmark. 103 | # Bucket will still be cleaned prior to benchmark. 104 | keep-data: false 105 | 106 | 107 | # The io section specifies custom IO properties for uploaded objects. 108 | io: 109 | # Use a custom prefix 110 | prefix: 111 | 112 | # Do not use separate prefix for each thread 113 | no-prefix: false 114 | 115 | # Add MD5 sum to uploads 116 | md5: false 117 | 118 | # Disable multipart uploads 119 | disable-multipart: false 120 | 121 | # Disable calculating sha256 on client side for uploads 122 | disable-sha256-payload: false 123 | 124 | # Server-side sse-s3 encrypt/decrypt objects 125 | sse-s3-encrypt: false 126 | 127 | # Encrypt/decrypt objects (using server-side encryption with random keys) 128 | sse-c-encrypt: false 129 | 130 | # Override storage class. 131 | # Default storage class will be used unless specified. 132 | storage-class: 133 | 134 | analyze: 135 | # Display additional analysis data. 136 | verbose: false 137 | # Only output for this host. 138 | host: '' 139 | # Only output for this operation. Can be 'GET', 'PUT', 'DELETE', etc. 140 | filter-op: '' 141 | # Split analysis into durations of this length. 142 | # Can be '1s', '5s', '1m', etc. 143 | segment-duration: 144 | # Output aggregated data as to file. 145 | out: 146 | # Additional time duration to skip when analyzing data. 147 | skip-duration: 148 | # Max operations to load for analysis. 149 | limit: 150 | # Skip this number of operations before starting analysis. 151 | offset: 152 | 153 | advanced: 154 | # Stress test only and discard output. 155 | stress: false 156 | 157 | # Print requests. 158 | debug: false 159 | 160 | # Disable HTTP Keep-Alive 161 | disable-http-keepalive: false 162 | 163 | # Enable HTTP2 support if server supports it 164 | http2: false 165 | 166 | # Rate limit each instance to this number of requests per second 167 | rps-limit: 168 | 169 | # Host selection algorithm. 170 | # Can be 'weighed' or 'roundrobin' 171 | host-select: weighed 172 | 173 | # "Resolve the host(s) ip(s) (including multiple A/AAAA records). 174 | # This can break SSL certificates, use --insecure if so 175 | resolve-host: false 176 | 177 | # Specify custom write socket buffer size in bytes 178 | sndbuf: 32768 179 | 180 | # Specify custom read socket buffer size in bytes 181 | rcvbuf: 32768 182 | 183 | # When running benchmarks open a webserver to fetch results remotely, eg: localhost:7762 184 | serve: 185 | -------------------------------------------------------------------------------- /yml-samples/stat.yml: -------------------------------------------------------------------------------- 1 | warp: 2 | api: v1 3 | 4 | # Benchmark to run. 5 | # Corresponds to warp [benchmark] command. 6 | benchmark: stat 7 | 8 | # Do not print any output. 9 | quiet: false 10 | 11 | # Disable terminal color output. 12 | no-color: false 13 | 14 | # Print results and errors as JSON. 15 | json: false 16 | 17 | # Output benchmark+profile data to this file. 18 | # By default a unique filename is generated. 19 | bench-data: 20 | 21 | # Connect to warp clients and run benchmarks there. 22 | # See https://github.com/minio/warp?tab=readme-ov-file#distributed-benchmarking 23 | # Can be a single value or a list. 24 | warp-client: 25 | 26 | # Run MinIO server profiling during benchmark; 27 | # possible values are 'cpu', 'cpuio', 'mem', 'block', 'mutex', 'threads' and 'trace'. 28 | # Can be single value or a list. 29 | server-profile: 30 | 31 | # Remote host parameters and connection info. 32 | remote: 33 | # Specify custom region 34 | region: us-east-1 35 | 36 | # Access key and Secret key 37 | access-key: 'Q3AM3UQ867SPQQA43P2F' 38 | secret-key: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' 39 | 40 | # Specify one or more hosts. 41 | # The benchmark will be run against all hosts concurrently. 42 | # Multiple servers can be specified with ellipsis notation; 43 | # for example '10.0.0.{1...10}:9000' specifies 10 hosts. 44 | # See more at https://github.com/minio/warp?tab=readme-ov-file#multiple-hosts 45 | host: 46 | - 'play.min.io' 47 | 48 | # Use TLS for calls. 49 | tls: true 50 | 51 | # Allow TLS with unverified certificates. 52 | insecure: false 53 | 54 | # Stream benchmark statistics to Influx DB instance. 55 | # See more at https://github.com/minio/warp?tab=readme-ov-file#influxdb-output 56 | influxdb: '' 57 | 58 | # Bucket to use for benchmark data. 59 | # 60 | # CAREFUL: ALL DATA WILL BE DELETED IN BUCKET! 61 | # 62 | # By default, 'warp-benchmark-bucket' will be created or used. 63 | bucket: 64 | 65 | # params specifies the benchmark parameters. 66 | # The fields here depend on the benchmark type. 67 | params: 68 | # Duration to run the benchmark. 69 | # Use 's' and 'm' to specify seconds and minutes. 70 | duration: 1m 71 | 72 | # Concurrent operations to run per warp instance. 73 | concurrent: 16 74 | 75 | # The number of objects to upload before starting the benchmark. 76 | # Upload enough objects to ensure that any remote caching is bypassed. 77 | objects: 5000 78 | 79 | # Properties of uploaded objects. 80 | obj: 81 | # Size of each uploaded object 82 | size: 1B 83 | 84 | # Number of versions to upload of each object 85 | versions: 2 86 | 87 | # Randomize the size of each object within certain constraints. 88 | # See https://github.com/minio/warp?tab=readme-ov-file#random-file-sizes 89 | rand-size: false 90 | 91 | # Force specific size of each multipart part. 92 | # Must be '5MB' or bigger. 93 | part-size: 94 | 95 | # Use automatic termination when traffic stabilizes. 96 | # Can not be used with distributed warp setup. 97 | # See https://github.com/minio/warp?tab=readme-ov-file#automatic-termination 98 | autoterm: 99 | enabled: false 100 | dur: 10s 101 | pct: 7.5 102 | 103 | # Do not clear bucket before or after running benchmarks. 104 | no-clear: false 105 | 106 | # Leave benchmark data. Do not run cleanup after benchmark. 107 | # Bucket will still be cleaned prior to benchmark. 108 | keep-data: false 109 | 110 | 111 | # The io section specifies custom IO properties for uploaded objects. 112 | io: 113 | # Use a custom prefix 114 | prefix: 115 | 116 | # Do not use separate prefix for each thread 117 | no-prefix: false 118 | 119 | # Add MD5 sum to uploads 120 | md5: false 121 | 122 | # Disable multipart uploads 123 | disable-multipart: false 124 | 125 | # Disable calculating sha256 on client side for uploads 126 | disable-sha256-payload: false 127 | 128 | # Server-side sse-s3 encrypt/decrypt objects 129 | sse-s3-encrypt: false 130 | 131 | # Encrypt/decrypt objects (using server-side encryption with random keys) 132 | sse-c-encrypt: false 133 | 134 | # Override storage class. 135 | # Default storage class will be used unless specified. 136 | storage-class: 137 | 138 | analyze: 139 | # Display additional analysis data. 140 | verbose: false 141 | # Only output for this host. 142 | host: '' 143 | # Only output for this operation. Can be 'GET', 'PUT', 'DELETE', etc. 144 | filter-op: '' 145 | # Split analysis into durations of this length. 146 | # Can be '1s', '5s', '1m', etc. 147 | segment-duration: 148 | # Output aggregated data as to file. 149 | out: 150 | # Additional time duration to skip when analyzing data. 151 | skip-duration: 152 | # Max operations to load for analysis. 153 | limit: 154 | # Skip this number of operations before starting analysis. 155 | offset: 156 | 157 | advanced: 158 | # Stress test only and discard output. 159 | stress: false 160 | 161 | # Print requests. 162 | debug: false 163 | 164 | # Disable HTTP Keep-Alive 165 | disable-http-keepalive: false 166 | 167 | # Enable HTTP2 support if server supports it 168 | http2: false 169 | 170 | # Rate limit each instance to this number of requests per second 171 | rps-limit: 172 | 173 | # Host selection algorithm. 174 | # Can be 'weighed' or 'roundrobin' 175 | host-select: weighed 176 | 177 | # "Resolve the host(s) ip(s) (including multiple A/AAAA records). 178 | # This can break SSL certificates, use --insecure if so 179 | resolve-host: false 180 | 181 | # Specify custom write socket buffer size in bytes 182 | sndbuf: 32768 183 | 184 | # Specify custom read socket buffer size in bytes 185 | rcvbuf: 32768 186 | 187 | # When running benchmarks open a webserver to fetch results remotely, eg: localhost:7762 188 | serve: 189 | -------------------------------------------------------------------------------- /yml-samples/zip.yml: -------------------------------------------------------------------------------- 1 | warp: 2 | api: v1 3 | 4 | # Benchmark to run. 5 | # Corresponds to warp [benchmark] command. 6 | benchmark: zip 7 | 8 | # Do not print any output. 9 | quiet: false 10 | 11 | # Disable terminal color output. 12 | no-color: false 13 | 14 | # Print results and errors as JSON. 15 | json: false 16 | 17 | # Output benchmark+profile data to this file. 18 | # By default a unique filename is generated. 19 | bench-data: 20 | 21 | # Connect to warp clients and run benchmarks there. 22 | # See https://github.com/minio/warp?tab=readme-ov-file#distributed-benchmarking 23 | # Can be a single value or a list. 24 | warp-client: 25 | 26 | # Run MinIO server profiling during benchmark; 27 | # possible values are 'cpu', 'cpuio', 'mem', 'block', 'mutex', 'threads' and 'trace'. 28 | # Can be single value or a list. 29 | server-profile: 30 | 31 | # Remote host parameters and connection info. 32 | remote: 33 | # Specify custom region 34 | region: us-east-1 35 | 36 | # Access key and Secret key 37 | access-key: 'Q3AM3UQ867SPQQA43P2F' 38 | secret-key: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' 39 | 40 | # Specify one or more hosts. 41 | # The benchmark will be run against all hosts concurrently. 42 | # Multiple servers can be specified with ellipsis notation; 43 | # for example '10.0.0.{1...10}:9000' specifies 10 hosts. 44 | # See more at https://github.com/minio/warp?tab=readme-ov-file#multiple-hosts 45 | host: 46 | - 'play.min.io' 47 | 48 | # Use TLS for calls. 49 | tls: true 50 | 51 | # Allow TLS with unverified certificates. 52 | insecure: false 53 | 54 | # Stream benchmark statistics to Influx DB instance. 55 | # See more at https://github.com/minio/warp?tab=readme-ov-file#influxdb-output 56 | influxdb: '' 57 | 58 | # Bucket to use for benchmark data. 59 | # 60 | # CAREFUL: ALL DATA WILL BE DELETED IN BUCKET! 61 | # 62 | # By default, 'warp-benchmark-bucket' will be created or used. 63 | bucket: 64 | 65 | # params specifies the benchmark parameters. 66 | # The fields here depend on the benchmark type. 67 | params: 68 | # Duration to run the benchmark. 69 | # Use 's' and 'm' to specify seconds and minutes. 70 | duration: 1m 71 | 72 | # Concurrent operations to run per warp instance. 73 | concurrent: 8 74 | 75 | # Number of files to upload in the zip file 76 | files: 10000 77 | 78 | # Properties of uploaded objects. 79 | obj: 80 | # Size of each uploaded object inside the zip file. 81 | size: 1KiB 82 | 83 | # Randomize the size of each object within certain constraints. 84 | # See https://github.com/minio/warp?tab=readme-ov-file#random-file-sizes 85 | rand-size: false 86 | 87 | # Use automatic termination when traffic stabilizes. 88 | # Can not be used with distributed warp setup. 89 | # See https://github.com/minio/warp?tab=readme-ov-file#automatic-termination 90 | autoterm: 91 | enabled: false 92 | dur: 10s 93 | pct: 7.5 94 | 95 | # Do not clear bucket before or after running benchmarks. 96 | no-clear: false 97 | 98 | # Leave benchmark data. Do not run cleanup after benchmark. 99 | # Bucket will still be cleaned prior to benchmark. 100 | keep-data: false 101 | 102 | 103 | # The io section specifies custom IO properties for uploaded objects. 104 | io: 105 | # Use a custom prefix 106 | prefix: 107 | 108 | # Do not use separate prefix for each thread 109 | no-prefix: false 110 | 111 | # Add MD5 sum to uploads 112 | md5: false 113 | 114 | # Disable multipart uploads 115 | disable-multipart: false 116 | 117 | # Disable calculating sha256 on client side for uploads 118 | disable-sha256-payload: false 119 | 120 | # Server-side sse-s3 encrypt/decrypt objects 121 | sse-s3-encrypt: false 122 | 123 | # Encrypt/decrypt objects (using server-side encryption with random keys) 124 | sse-c-encrypt: false 125 | 126 | # Override storage class. 127 | # Default storage class will be used unless specified. 128 | storage-class: 129 | 130 | analyze: 131 | # Display additional analysis data. 132 | verbose: false 133 | # Only output for this host. 134 | host: '' 135 | # Only output for this operation. Can be 'GET', 'PUT', 'DELETE', etc. 136 | filter-op: '' 137 | # Split analysis into durations of this length. 138 | # Can be '1s', '5s', '1m', etc. 139 | segment-duration: 140 | # Output aggregated data as to file. 141 | out: 142 | # Additional time duration to skip when analyzing data. 143 | skip-duration: 144 | # Max operations to load for analysis. 145 | limit: 146 | # Skip this number of operations before starting analysis. 147 | offset: 148 | 149 | advanced: 150 | # Stress test only and discard output. 151 | stress: false 152 | 153 | # Print requests. 154 | debug: false 155 | 156 | # Disable HTTP Keep-Alive 157 | disable-http-keepalive: false 158 | 159 | # Enable HTTP2 support if server supports it 160 | http2: false 161 | 162 | # Rate limit each instance to this number of requests per second 163 | rps-limit: 164 | 165 | # Host selection algorithm. 166 | # Can be 'weighed' or 'roundrobin' 167 | host-select: weighed 168 | 169 | # "Resolve the host(s) ip(s) (including multiple A/AAAA records). 170 | # This can break SSL certificates, use --insecure if so 171 | resolve-host: false 172 | 173 | # Specify custom write socket buffer size in bytes 174 | sndbuf: 32768 175 | 176 | # Specify custom read socket buffer size in bytes 177 | rcvbuf: 32768 178 | 179 | # When running benchmarks open a webserver to fetch results remotely, eg: localhost:7762 180 | serve: 181 | --------------------------------------------------------------------------------