├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd └── usl │ ├── example.csv │ ├── usl.go │ └── usl_test.go ├── go.mod ├── go.sum ├── measurement.go ├── measurement_test.go ├── model.go └── model_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 9 | uses: actions/setup-go@v2 10 | with: 11 | go-version: 1.16 12 | id: go 13 | 14 | - name: Check out code 15 | uses: actions/checkout@v2.4.0 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | 23 | - name: Test 24 | run: go test -v ./... 25 | 26 | - name: Lint 27 | uses: golangci/golangci-lint-action@v2.5.2 28 | with: 29 | version: v1.37 30 | skip-go-installation: true 31 | skip-pkg-cache: true 32 | skip-build-cache: true 33 | 34 | - name: Cross-compile 35 | uses: goreleaser/goreleaser-action@v2.8.1 36 | if: github.ref == 'refs/heads/main' 37 | with: 38 | version: latest 39 | args: build --snapshot 40 | 41 | - name: Release 42 | uses: goreleaser/goreleaser-action@v2.8.1 43 | if: startsWith(github.ref, 'refs/tags/') 44 | with: 45 | version: latest 46 | args: release --rm-dist 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | # goreleaser staging 20 | dist 21 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - asciicheck 5 | - bodyclose 6 | - cyclop 7 | - deadcode 8 | - depguard 9 | - dogsled 10 | - dupl 11 | - durationcheck 12 | - errcheck 13 | - errorlint 14 | - exhaustive 15 | - exportloopref 16 | - funlen 17 | - gci 18 | - gochecknoglobals 19 | - gochecknoinits 20 | - gocognit 21 | - goconst 22 | - gocritic 23 | - gocyclo 24 | - godot 25 | - godox 26 | - goerr113 27 | - gofmt 28 | - gofumpt 29 | - goimports 30 | - golint 31 | - gomodguard 32 | - goprintffuncname 33 | - gosec 34 | - gosimple 35 | - govet 36 | - ifshort 37 | - ineffassign 38 | - interfacer 39 | - lll 40 | - makezero 41 | - maligned 42 | - misspell 43 | - nakedret 44 | - nestif 45 | - noctx 46 | - nolintlint 47 | - paralleltest 48 | - prealloc 49 | - predeclared 50 | - revive 51 | - rowserrcheck 52 | - scopelint 53 | - sqlclosecheck 54 | - staticcheck 55 | - structcheck 56 | - stylecheck 57 | - thelper 58 | - tparallel 59 | - unconvert 60 | - unparam 61 | - unused 62 | - varcheck 63 | - whitespace 64 | - wsl 65 | # - exhaustivestruct 66 | # - forbidigo 67 | # - goheader 68 | # - gomnd 69 | # - nlreturn 70 | # - testpackage 71 | # - typecheck 72 | # - wrapcheck 73 | 74 | linters-settings: 75 | funlen: 76 | lines: 150 77 | gocognit: 78 | min-complexity: 10 79 | gocyclo: 80 | min-complexity: 10 81 | gofumpt: 82 | extra-rules: true 83 | maligned: 84 | suggest-new: true 85 | nolintlint: 86 | allow-leading-space: false 87 | require-explanation: true 88 | require-specific: true 89 | wsl: 90 | force-err-cuddling: true 91 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | dir: ./cmd/usl 5 | goos: 6 | - darwin 7 | - linux 8 | - windows 9 | goarch: 10 | - amd64 11 | - arm64 12 | mod_timestamp: "{{.CommitTimestamp}}" 13 | ldflags: 14 | - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.CommitDate}} 15 | flags: 16 | - -trimpath 17 | archives: 18 | - format_overrides: 19 | - goos: windows 20 | format: zip 21 | checksum: 22 | name_template: 'checksums.txt' 23 | snapshot: 24 | name_template: "{{.Tag}}-next" 25 | changelog: 26 | skip: true 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | usl 2 | === 3 | 4 | `usl` is a Go modeler for [Dr. Neil Gunther][NJG]'s [Universal Scalability Law][USL] as described in 5 | [Baron Schwartz][BS]'s book [Practical Scalability Analysis with the Universal Scalability 6 | Law][PSA]. 7 | 8 | Given a handful of measurements of any two [Little's Law][LL] parameters--throughput, latency, and 9 | concurrency--the [USL][USL] allows you to make predictions about any of those parameters' values 10 | given an arbitrary value for any another parameter. For example, given a set of measurements of 11 | concurrency and throughput, the [USL][USL] will allow you to predict what a system's average latency 12 | will look like at a particular throughput, or how many servers you'll need to process requests and 13 | stay under your SLA's latency requirements. 14 | 15 | The model coefficients and predictions should be within 0.001% of those listed in the book. 16 | 17 | ## How to use this 18 | 19 | As an example, consider doing load testing and capacity planning for an HTTP server. To model the 20 | behavior of the system using the [USL][USL], you must first gather a set of measurements of the 21 | system. These measurements must be of two of the three parameters of [Little's Law][LL]: mean 22 | response time (in seconds), throughput (in requests per second), and concurrency (i.e. the number of 23 | concurrent clients). 24 | 25 | Because response time tends to be a property of load (i.e. it rises as throughput or concurrency 26 | rises), the dependent variable in your tests should be mean response time. This leaves either 27 | throughput or concurrency as your independent variable, but thanks to [Little's Law][LL] it doesn't 28 | matter which one you use. For the purposes of discussion, let's say you measure throughput as a 29 | function of the number of concurrent clients working at a fixed rate (e.g. you used 30 | [`wrk2`][wrk2]). 31 | 32 | After you're done load testing, you should have a set of measurements shaped like this: 33 | 34 | |concurrency|throughput| 35 | |-----------|----------| 36 | | 1| 65| 37 | | 18| 996| 38 | | 36| 1652| 39 | | 72| 1853| 40 | | 108| 1829| 41 | | 144| 1775| 42 | | 216| 1702| 43 | 44 | Now you can build a model and begin estimating things. 45 | 46 | ### As A CLI Tool 47 | 48 | ``` 49 | $ go get github.com/codahale/usl/cmd/usl 50 | ``` 51 | 52 | ``` 53 | $ cat measurements.csv 54 | 1,65 55 | 18,996 56 | 36,1652 57 | 72,1853 58 | etc. 59 | ``` 60 | 61 | ``` 62 | $ usl measurements.csv 10 50 100 150 200 250 300 63 | USL parameters: σ=0.0277299, κ=0.000104343, λ=89.9878 64 | max throughput: 1883.76, max concurrency: 96 65 | contention constrained 66 | 67 | | 68 | 2.1 k + 69 | 2.0 k + ***X******@***X********* 70 | 1.8 k + ***** **X********** 71 | 1.7 k + X ** 72 | 1.6 k + ** 73 | 1.5 k + ** 74 | 1.3 k + * 75 | 1.2 k + * 76 | 1.1 k + X* 77 | 975 + * .------------------. 78 | 853 + * |****** Predicted | 79 | 731 + * | X Actual | 80 | 609 + * | @ Peak | 81 | 487 + * '------------------' 82 | 366 + * 83 | 244 +* 84 | 1122 X----+------+-----+-----+-----+------+-----+-----+-----+-----+- 85 | 19 38 58 77 96 115 134 154 173 192 86 | 87 | 10.000000,714.778987 88 | 50.000000,1721.000603 89 | 100.000000,1883.278957 90 | 150.000000,1808.481681 91 | 200.000000,1686.571797 92 | 250.000000,1562.279331 93 | 300.000000,1447.463805 94 | ``` 95 | 96 | ### As A Go Library 97 | 98 | 99 | ```go 100 | import ( 101 | "fmt" 102 | 103 | "github.com/codahale/usl" 104 | ) 105 | 106 | func main() { 107 | measurements := []usl.Measurement{ 108 | usl.ConcurrencyAndThroughput(1, 955.16), 109 | usl.ConcurrencyAndThroughput(2, 1878.91), 110 | usl.ConcurrencyAndThroughput(3, 2688.01), // etc 111 | } 112 | 113 | model := usl.Build(measurements) 114 | for n := 10; n < 200; n += 10 { 115 | fmt.Printf("At %d concurrent clients, expect %f req/sec\n", 116 | n, model.ThroughputAtConcurrency(float64(n))) 117 | } 118 | } 119 | ``` 120 | 121 | ## Performance 122 | 123 | Building models is pretty fast: 124 | 125 | ``` 126 | pkg: github.com/codahale/usl 127 | BenchmarkBuild-8 2242 500232 ns/op 128 | ``` 129 | 130 | ## Further reading 131 | 132 | I strongly recommend [Practical Scalability Analysis with the Universal Scalability Law][PSA], a 133 | free e-book by [Baron Schwartz][BS], author of [High Performance MySQL][MySQL] and CEO of 134 | [VividCortex][VC]. Trying to use this library without actually understanding the concepts behind 135 | [Little's Law][LL], [Amdahl's Law][AL], and the [Universal Scalability Law][USL] will be difficult 136 | and potentially misleading. 137 | 138 | I also [wrote a blog post about my Java implementation of USL][usl4j]. 139 | 140 | ## License 141 | 142 | Copyright © 2021 Coda Hale 143 | 144 | Distributed under the Apache License 2.0. 145 | 146 | [NJG]: http://www.perfdynamics.com/Bio/njg.html 147 | [AL]: https://en.wikipedia.org/wiki/Amdahl%27s_law 148 | [LL]: https://en.wikipedia.org/wiki/Little%27s_law 149 | [PSA]: https://www.vividcortex.com/resources/universal-scalability-law/ 150 | [USL]: http://www.perfdynamics.com/Manifesto/USLscalability.html 151 | [BS]: https://www.xaprb.com/ 152 | [MySQL]: http://shop.oreilly.com/product/0636920022343.do 153 | [VC]: https://www.vividcortex.com/ 154 | [wrk2]: https://github.com/giltene/wrk2 155 | [usl4j]: https://codahale.com/usl4j-and-you/ 156 | -------------------------------------------------------------------------------- /cmd/usl/example.csv: -------------------------------------------------------------------------------- 1 | 1,65 2 | 18,996 3 | 36,1652 4 | 72,1853 5 | 108,1829 6 | 144,1775 7 | 216,1702 8 | -------------------------------------------------------------------------------- /cmd/usl/usl.go: -------------------------------------------------------------------------------- 1 | // USL is a modeler for the Universal Scalability Law, which can be used in system testing and 2 | // capacity planning. 3 | // 4 | // As an example, consider doing load testing and capacity planning for an HTTP server. To use USL, 5 | // we must first gather a set of measurements of the system. These measurements will consist of 6 | // pairs of simultaneous measurements of the independent and dependent variables. With an HTTP 7 | // server, it might be tempting to use the rate as the independent variable, but this is a mistake. 8 | // The rate of requests being handled by the server is actually itself a dependent variable of two 9 | // other independent variables: the number of concurrent users and the rate at which users send 10 | // requests. 11 | // 12 | // As we do our capacity planning, we make the observation that users of our system do ~10 req/sec. 13 | // (Or, more commonly, we assume this based on a hunch.) By holding this constant, we leave the 14 | // number of concurrent users as the single remaining independent variable. 15 | // 16 | // Our load testing, then, should consist of running a series of tests with an increasing number of 17 | // simulated users, each performing ~10 req/sec. While the number of users to test with depends 18 | // heavily on your system, you should be testing at least six different concurrency levels. You 19 | // should do one test with a single user in order to determine the performance of an uncontended 20 | // system. 21 | // 22 | // After our load testing is done, we should have a CSV file which consists of a series of 23 | // (concurrency, throughput) pairs of measurements: 24 | // 25 | // 1,65 26 | // 18,996 27 | // 36,1652 28 | // 72,1853 29 | // 108,1829 30 | // 144,1775 31 | // 216,1702 32 | // 33 | // We can then run the USL binary: 34 | // 35 | // usl data.csv 36 | // 37 | // USL parses the given CSV file as a series of (concurrency, throughput) points, calculates the USL 38 | // parameters using quadratic regression, and then prints out the details of the model, along with a 39 | // graph of the model's predictions and the given measurements. 40 | // 41 | // Finally, we can provide USL a series of additional data points to provide 42 | // estimates for: 43 | // 44 | // usl data.csv 128 256 512 45 | // 46 | // USL will output the data in CSV format on STDOUT. 47 | // 48 | // For more information, see http://www.perfdynamics.com/Manifesto/USLscalability.html. 49 | package main 50 | 51 | import ( 52 | "encoding/csv" 53 | "fmt" 54 | "os" 55 | "strconv" 56 | 57 | "github.com/alecthomas/kong" 58 | "github.com/codahale/usl" 59 | "github.com/vdobler/chart" 60 | "github.com/vdobler/chart/txtg" 61 | ) 62 | 63 | func main() { 64 | if err := run(); err != nil { 65 | _, _ = fmt.Fprintln(os.Stderr, err) 66 | 67 | os.Exit(-1) 68 | } 69 | } 70 | 71 | func run() error { 72 | //nolint:maligned // ordering of fields matters 73 | var cli struct { 74 | InputPath string `arg:"" type:"existingfile" help:"The CSV file measurements of the system."` 75 | Predictions []float64 `arg:"" optional:"" help:"Predict throughput at the given concurrency levels."` 76 | ConcurrencyColumn int `short:"N" default:"1" help:"The column index of concurrency values."` 77 | LatencyColumn int `short:"R" default:"2" help:"The column index of latency values."` 78 | SkipHeaders bool `default:"false" help:"Skip the first line of the file."` 79 | Width int `short:"W" default:"74" help:"The width of the graph in chars."` 80 | Height int `short:"H" default:"20" help:"The height of the graph in chars."` 81 | NoGraph bool `default:"false" help:"Don't display the graph.'"` 82 | Version kong.VersionFlag `help:"Display the application version."` 83 | } 84 | 85 | ctx := kong.Parse(&cli, kong.Vars{"version": version}) 86 | if ctx.Error != nil { 87 | _, _ = fmt.Fprintln(os.Stderr, ctx.Error) 88 | os.Exit(1) 89 | } 90 | 91 | measurements, err := parseCSV(cli.InputPath, cli.ConcurrencyColumn, cli.LatencyColumn, cli.SkipHeaders) 92 | if err != nil { 93 | return fmt.Errorf("error parsing %q: %w", cli.InputPath, err) 94 | } 95 | 96 | m, err := usl.Build(measurements) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | printModel(m, measurements, cli.NoGraph, cli.Width, cli.Height) 102 | 103 | printPredictions(m, cli.Predictions) 104 | 105 | return nil 106 | } 107 | 108 | func printModel(m *usl.Model, measurements []usl.Measurement, noGraph bool, width, height int) { 109 | _, _ = fmt.Fprintf(os.Stderr, "USL parameters: σ=%.6g, κ=%.6g, λ=%.6g\n", m.Sigma, m.Kappa, m.Lambda) 110 | _, _ = fmt.Fprintf(os.Stderr, "\tmax throughput: %.6g, max concurrency: %.6g\n", m.MaxThroughput(), m.MaxConcurrency()) 111 | 112 | if m.ContentionConstrained() { 113 | _, _ = fmt.Fprintln(os.Stderr, "\tcontention constrained") 114 | } 115 | 116 | if m.CoherencyConstrained() { 117 | _, _ = fmt.Fprintln(os.Stderr, "\tcoherence constrained") 118 | } 119 | 120 | if m.Limitless() { 121 | _, _ = fmt.Fprintln(os.Stderr, "\tlimitless") 122 | } 123 | 124 | if !noGraph { 125 | x := make([]float64, len(measurements)) 126 | y := make([]float64, len(measurements)) 127 | 128 | for i, m := range measurements { 129 | x[i] = m.Concurrency 130 | y[i] = m.Throughput 131 | } 132 | 133 | c := chart.ScatterChart{} 134 | c.Key.Pos = "ibr" 135 | c.XRange.Fixed(1, m.MaxConcurrency()*2, (m.MaxConcurrency()*2)/10) 136 | c.YRange.Fixed(0, m.MaxThroughput()*1.1, 0) 137 | c.NSamples = len(measurements) 138 | c.AddFunc("Predicted", m.ThroughputAtConcurrency, 139 | chart.PlotStyleLines, chart.AutoStyle(6, false)) 140 | c.AddDataPair("Actual", x, y, chart.PlotStylePoints, chart.AutoStyle(5, false)) 141 | c.AddDataPair("Peak", []float64{m.MaxConcurrency()}, []float64{m.MaxThroughput()}, 142 | chart.PlotStylePoints, chart.AutoStyle(7, false)) 143 | 144 | txt := txtg.New(width, height) 145 | c.Plot(txt) 146 | 147 | _, _ = fmt.Fprint(os.Stderr, txt) 148 | } 149 | 150 | _, _ = fmt.Fprintln(os.Stderr) 151 | } 152 | 153 | func printPredictions(m *usl.Model, args []float64) { 154 | for _, n := range args { 155 | fmt.Printf("%f,%f\n", n, m.ThroughputAtConcurrency(n)) 156 | } 157 | } 158 | 159 | func parseCSV(filename string, nCol, rCol int, skipHeaders bool) ([]usl.Measurement, error) { 160 | measurements := make([]usl.Measurement, 0, 100) 161 | 162 | f, err := os.Open(filename) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | defer func() { _ = f.Close() }() 168 | 169 | r := csv.NewReader(f) 170 | 171 | lines, err := r.ReadAll() 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | if skipHeaders { 177 | lines = lines[1:] 178 | } 179 | 180 | for i, line := range lines { 181 | n, x, err := parseLine(i, nCol, rCol, line) 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | measurements = append(measurements, usl.ConcurrencyAndThroughput(n, x)) 187 | } 188 | 189 | return measurements, nil 190 | } 191 | 192 | //nolint:goerr113 // not a package 193 | func parseLine(i, nCol, xCol int, line []string) (uint64, float64, error) { 194 | if len(line) != 2 { 195 | return 0, 0, fmt.Errorf("invalid line at line %d", i+1) 196 | } 197 | 198 | n, err := strconv.ParseUint(line[nCol-1], 10, 64) 199 | if err != nil { 200 | return 0, 0, fmt.Errorf("error at line %d, column %d: %w", i+1, nCol, err) 201 | } 202 | 203 | x, err := strconv.ParseFloat(line[xCol-1], 64) 204 | if err != nil { 205 | return 0, 0, fmt.Errorf("error at line %d, column %d: %w", i+1, xCol, err) 206 | } 207 | 208 | return n, x, nil 209 | } 210 | 211 | var version = "dev" 212 | -------------------------------------------------------------------------------- /cmd/usl/usl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/codahale/gubbins/assert" 9 | "github.com/codahale/usl" 10 | "github.com/google/go-cmp/cmp/cmpopts" 11 | ) 12 | 13 | func TestParsing(t *testing.T) { 14 | t.Parallel() 15 | 16 | want := []usl.Measurement{ 17 | usl.ConcurrencyAndThroughput(1, 65), 18 | usl.ConcurrencyAndThroughput(18, 996), 19 | usl.ConcurrencyAndThroughput(36, 1652), 20 | usl.ConcurrencyAndThroughput(72, 1853), 21 | usl.ConcurrencyAndThroughput(108, 1829), 22 | usl.ConcurrencyAndThroughput(144, 1775), 23 | usl.ConcurrencyAndThroughput(216, 1702), 24 | } 25 | 26 | got, err := parseCSV("example.csv", 1, 2, false) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | assert.Equal(t, "measurements", want, got, 32 | cmpopts.EquateApprox(0.001, 0.001)) 33 | } 34 | 35 | func TestBadLine(t *testing.T) { 36 | t.Parallel() 37 | 38 | _, _, err := parseLine(0, 1, 2, []string{"funk"}) 39 | if err == nil { 40 | t.Fatalf("should have failed") 41 | } 42 | } 43 | 44 | func TestBadConcurrency(t *testing.T) { 45 | t.Parallel() 46 | 47 | _, _, err := parseLine(0, 1, 2, []string{"f", "1"}) 48 | if err == nil { 49 | t.Fatalf("should have failed") 50 | } 51 | } 52 | 53 | func TestBadThroughput(t *testing.T) { 54 | t.Parallel() 55 | 56 | _, _, err := parseLine(0, 1, 2, []string{"1", "f"}) 57 | if err == nil { 58 | t.Fatalf("should have failed") 59 | } 60 | } 61 | 62 | func TestMainRun(t *testing.T) { 63 | t.Parallel() 64 | 65 | stdout, stderr := fakeMain(t, "example.csv", "1", "2", "3") 66 | 67 | assert.Equal(t, "stdout", 68 | `1.000000,89.987785 69 | 2.000000,175.083978 70 | 3.000000,255.626353 71 | `, 72 | string(stdout)) 73 | 74 | assert.Equal(t, "stderr", 75 | `USL parameters: σ=0.0277299, κ=0.000104343, λ=89.9878 76 | max throughput: 1883.76, max concurrency: 96 77 | contention constrained 78 | 79 | | 80 | 2.1 k + 81 | 2.0 k + ***X******@***X********* 82 | 1.8 k + ***** **X********** 83 | 1.7 k + X ** 84 | 1.6 k + ** 85 | 1.5 k + ** 86 | 1.3 k + * 87 | 1.2 k + * 88 | 1.1 k + X* 89 | 975 + * .------------------. 90 | 853 + * |****** Predicted | 91 | 731 + * | X Actual | 92 | 609 + * | @ Peak | 93 | 487 + * '------------------' 94 | 366 + * 95 | 244 +* 96 | 1122 X----+------+-----+-----+-----+------+-----+-----+-----+-----+- 97 | 19 38 58 77 96 115 134 154 173 192 98 | 99 | `, 100 | string(stderr)) 101 | } 102 | 103 | func fakeMain(t *testing.T, args ...string) ([]byte, []byte) { 104 | t.Helper() 105 | 106 | stdout, err := ioutil.TempFile(os.TempDir(), "stdout") 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | defer func() { _ = stdout.Close() }() 112 | 113 | stderr, err := ioutil.TempFile(os.TempDir(), "stderr") 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | defer func() { _ = stderr.Close() }() 119 | 120 | oldStdout := os.Stdout 121 | oldStderr := os.Stderr 122 | 123 | defer func() { 124 | os.Stdout = oldStdout 125 | os.Stderr = oldStderr 126 | }() 127 | 128 | os.Stdout = stdout 129 | os.Stderr = stderr 130 | 131 | os.Args = append([]string{"usl"}, args...) 132 | 133 | main() 134 | 135 | err = stdout.Sync() 136 | if err != nil { 137 | t.Error(err) 138 | } 139 | 140 | err = stderr.Sync() 141 | if err != nil { 142 | t.Error(err) 143 | } 144 | 145 | stdoutData, err := ioutil.ReadFile(stdout.Name()) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | 150 | stderrData, err := ioutil.ReadFile(stderr.Name()) 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | 155 | return stdoutData, stderrData 156 | } 157 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codahale/usl 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/alecthomas/kong v0.3.0 7 | github.com/codahale/gubbins v0.0.1 8 | github.com/google/go-cmp v0.5.7 9 | github.com/maorshutman/lm v0.0.0-20190501150544-7c8d1397ebf3 10 | github.com/vdobler/chart v1.0.0 11 | gonum.org/v1/gonum v0.9.3 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= 3 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 4 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 5 | github.com/ajstarks/svgo v0.0.0-20181006003313-6ce6a3bcf6cd/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 6 | github.com/alecthomas/kong v0.3.0 h1:qOLFzu0dGPNz8z5TiXGzgW3gb3RXfWVJKeAxcghVW88= 7 | github.com/alecthomas/kong v0.3.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0= 8 | github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= 9 | github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 10 | github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 11 | github.com/codahale/gubbins v0.0.1 h1:BQq8wKhnQPvkfkCu1MnU73XWmv+5HG3p4TPYUJhVnGA= 12 | github.com/codahale/gubbins v0.0.1/go.mod h1:GEbM0qmOZZmIBeSy/0elVxeaNbipRIelCsjkdG/+g+4= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 17 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 18 | github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= 19 | github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= 20 | github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= 21 | github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= 22 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= 23 | github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 24 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 25 | github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= 26 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 29 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 30 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 31 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 32 | github.com/llgcode/draw2d v0.0.0-20180825133448-f52c8a71aff0/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA= 33 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY= 34 | github.com/maorshutman/lm v0.0.0-20190501150544-7c8d1397ebf3 h1:zTRDA1MncZ35UYc2fBcwGZbL0AZkLwuPquMSXLnaWVI= 35 | github.com/maorshutman/lm v0.0.0-20190501150544-7c8d1397ebf3/go.mod h1:yDDTwtUPUoGH8NXn/97kSCbeV3M2BKHi7L1so+qSc/w= 36 | github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= 37 | github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 38 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 40 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 46 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 47 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 48 | github.com/vdobler/chart v1.0.0 h1:ySWmgHJtBsb7/SItvKb+VM3Nxb0SksDIjZhSbiK+Wi0= 49 | github.com/vdobler/chart v1.0.0/go.mod h1:gRwLtqIJLDw1CkK9kxJXv3X9OaMfM4dYsbZtWtVLxvM= 50 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 51 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 52 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 53 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 54 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 55 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 56 | golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs= 57 | golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= 58 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 59 | golang.org/x/image v0.0.0-20181030002151-69cc3646b96e/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 60 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 61 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 62 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 63 | golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 64 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 65 | golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 66 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 67 | golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 68 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 69 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 70 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 71 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 72 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 78 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 79 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 80 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 81 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 82 | golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e h1:1xWUkZQQ9Z9UuZgNaIR6OQOE7rUFglXUUBZlO+dGg6I= 83 | golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 84 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 86 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 87 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 88 | gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= 89 | gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s= 90 | gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= 91 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= 92 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 93 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 94 | gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= 95 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 97 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 98 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 99 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 100 | -------------------------------------------------------------------------------- /measurement.go: -------------------------------------------------------------------------------- 1 | package usl 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Measurement is a simultaneous measurement of at least two of the parameters of Little's Law: 9 | // concurrency, throughput, and latency. The third parameter is inferred from the other two. 10 | type Measurement struct { 11 | Concurrency float64 // The average number of concurrent events. 12 | Throughput float64 // The long-term average arrival rate of events, in events/sec. 13 | Latency float64 // The average duration of events in seconds. 14 | } 15 | 16 | func (m *Measurement) String() string { 17 | return fmt.Sprintf("(n=%v,x=%v,r=%v)", m.Concurrency, m.Throughput, m.Latency) 18 | } 19 | 20 | // ConcurrencyAndLatency returns a measurement of a system's latency at a given level of 21 | // concurrency. The throughput of the system is derived via Little's Law. 22 | func ConcurrencyAndLatency(n uint64, r time.Duration) Measurement { 23 | return Measurement{ 24 | Concurrency: float64(n), // L 25 | Throughput: float64(n) / r.Seconds(), // λ=L/W 26 | Latency: r.Seconds(), // W 27 | } 28 | } 29 | 30 | // ConcurrencyAndThroughput returns a measurement of a system's throughput at a given level of 31 | // concurrency. The latency of the system is derived via Little's Law. 32 | func ConcurrencyAndThroughput(n uint64, x float64) Measurement { 33 | return Measurement{ 34 | Concurrency: float64(n), // L 35 | Throughput: x, // λ 36 | Latency: float64(n) / x, // W=L/λ 37 | } 38 | } 39 | 40 | // ThroughputAndLatency returns a measurement of a system's latency at a given level of throughput. 41 | // The concurrency of the system is derived via Little's Law. 42 | func ThroughputAndLatency(x float64, r time.Duration) Measurement { 43 | return Measurement{ 44 | Concurrency: x * r.Seconds(), // L=λW 45 | Throughput: x, // λ 46 | Latency: r.Seconds(), // W 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /measurement_test.go: -------------------------------------------------------------------------------- 1 | package usl 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/codahale/gubbins/assert" 8 | ) 9 | 10 | func TestMeasurement_String(t *testing.T) { 11 | t.Parallel() 12 | 13 | m := Measurement{Concurrency: 1, Throughput: 2, Latency: 3} 14 | 15 | assert.Equal(t, "String", "(n=1,x=2,r=3)", m.String()) 16 | } 17 | 18 | func TestConcurrencyAndLatency(t *testing.T) { 19 | t.Parallel() 20 | 21 | m := ConcurrencyAndLatency(3, 600*time.Millisecond) 22 | 23 | assert.Equal(t, "Concurrency", 3.0, m.Concurrency, epsilon) 24 | assert.Equal(t, "Latency", 0.6, m.Latency, epsilon) 25 | assert.Equal(t, "Throughput", 5.0, m.Throughput, epsilon) 26 | } 27 | 28 | func TestConcurrencyAndThroughput(t *testing.T) { 29 | t.Parallel() 30 | 31 | m := ConcurrencyAndThroughput(3, 5) 32 | 33 | assert.Equal(t, "Concurrency", 3.0, m.Concurrency, epsilon) 34 | assert.Equal(t, "Latency", 0.6, m.Latency, epsilon) 35 | assert.Equal(t, "Throughput", 5.0, m.Throughput, epsilon) 36 | } 37 | 38 | func TestThroughputAndLatency(t *testing.T) { 39 | t.Parallel() 40 | 41 | m := ThroughputAndLatency(5, 600*time.Millisecond) 42 | 43 | assert.Equal(t, "Concurrency", 3.0, m.Concurrency, epsilon) 44 | assert.Equal(t, "Latency", 0.6, m.Latency, epsilon) 45 | assert.Equal(t, "Throughput", 5.0, m.Throughput, epsilon) 46 | } 47 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | // Package usl provides functionality to build Universal Scalability Law models 2 | // from sets of observed measurements. 3 | package usl 4 | 5 | import ( 6 | "fmt" 7 | "math" 8 | 9 | "github.com/maorshutman/lm" 10 | "gonum.org/v1/gonum/floats" 11 | ) 12 | 13 | // Model is a Universal Scalability Law model. 14 | type Model struct { 15 | Sigma float64 // The model's coefficient of contention, σ. 16 | Kappa float64 // The model's coefficient of crosstalk/coherency, κ. 17 | Lambda float64 // The model's coefficient of performance, λ. 18 | } 19 | 20 | func (m *Model) String() string { 21 | return fmt.Sprintf("Model{σ=%v,κ=%v,λ=%v}", m.Sigma, m.Kappa, m.Lambda) 22 | } 23 | 24 | // ThroughputAtConcurrency returns the expected throughput given a number of concurrent events, 25 | // X(N). 26 | // 27 | // See "Practical Scalability Analysis with the Universal Scalability Law, Equation 3". 28 | func (m *Model) ThroughputAtConcurrency(n float64) float64 { 29 | return (m.Lambda * n) / (1 + (m.Sigma * (n - 1)) + (m.Kappa * n * (n - 1))) 30 | } 31 | 32 | // LatencyAtConcurrency returns the expected mean latency given a number of concurrent events, 33 | // R(N). 34 | // 35 | // See "Practical Scalability Analysis with the Universal Scalability Law, Equation 6". 36 | func (m *Model) LatencyAtConcurrency(n float64) float64 { 37 | return (1 + (m.Sigma * (n - 1)) + (m.Kappa * n * (n - 1))) / m.Lambda 38 | } 39 | 40 | // MaxConcurrency returns the maximum expected number of concurrent events the system can handle, 41 | // Nmax. 42 | // 43 | // See "Practical Scalability Analysis with the Universal Scalability Law, Equation 4". 44 | func (m *Model) MaxConcurrency() float64 { 45 | return math.Floor(math.Sqrt((1 - m.Sigma) / m.Kappa)) 46 | } 47 | 48 | // MaxThroughput returns the maximum expected throughput the system can handle, Xmax. 49 | func (m Model) MaxThroughput() float64 { 50 | return m.ThroughputAtConcurrency(m.MaxConcurrency()) 51 | } 52 | 53 | // LatencyAtThroughput returns the expected mean latency given a throughput, R(X). 54 | // 55 | // See "Practical Scalability Analysis with the Universal Scalability Law, Equation 8". 56 | func (m *Model) LatencyAtThroughput(x float64) float64 { 57 | return (m.Sigma - 1) / (m.Sigma*x - m.Lambda) 58 | } 59 | 60 | // ThroughputAtLatency returns the expected throughput given a mean latency, X(R). 61 | // 62 | // See "Practical Scalability Analysis with the Universal Scalability Law, Equation 9". 63 | func (m *Model) ThroughputAtLatency(r float64) float64 { 64 | return (math.Sqrt(math.Pow(m.Sigma, 2)+math.Pow(m.Kappa, 2)+ 65 | 2*m.Kappa*(2*m.Lambda*r+m.Sigma-2)) - m.Kappa + m.Sigma) / (2.0 * m.Kappa * r) 66 | } 67 | 68 | // ConcurrencyAtLatency returns the expected number of concurrent events at a particular mean 69 | // latency, N(R). 70 | // 71 | // See "Practical Scalability Analysis with the Universal Scalability Law, Equation 10". 72 | func (m *Model) ConcurrencyAtLatency(r float64) float64 { 73 | return (m.Kappa - m.Sigma + 74 | math.Sqrt(math.Pow(m.Sigma, 2)+ 75 | math.Pow(m.Kappa, 2)+ 76 | 2*m.Kappa*((2*m.Lambda*r)+m.Sigma-2))) / (2 * m.Kappa) 77 | } 78 | 79 | // ConcurrencyAtThroughput returns the expected number of concurrent events at a particular 80 | // throughput, N(X). 81 | func (m *Model) ConcurrencyAtThroughput(x float64) float64 { 82 | return m.LatencyAtThroughput(x) * x 83 | } 84 | 85 | // ContentionConstrained returns true if the system is constrained by contention. 86 | func (m *Model) ContentionConstrained() bool { 87 | return m.Sigma > m.Kappa 88 | } 89 | 90 | // CoherencyConstrained returns true if the system is constrained by coherency costs. 91 | func (m *Model) CoherencyConstrained() bool { 92 | return m.Sigma < m.Kappa 93 | } 94 | 95 | // Limitless returns true if the system is linearly scalable. 96 | func (m *Model) Limitless() bool { 97 | return m.Kappa == 0 98 | } 99 | 100 | // Build returns a model whose parameters are generated from the given measurements. 101 | // 102 | // Finds a set of coefficients for the equation y = λx/(1+σ(x-1)+κx(x-1)) which best fit the 103 | // observed values using unconstrained least-squares regression. The resulting values for λ, κ, and 104 | // σ are the parameters of the returned model. 105 | func Build(measurements []Measurement) (m *Model, err error) { 106 | if len(measurements) < minMeasurements { 107 | return nil, ErrInsufficientMeasurements 108 | } 109 | 110 | // Calculate x/n for all measurements. 111 | xn := make([]float64, len(measurements)) 112 | for i, m := range measurements { 113 | xn[i] = m.Throughput / m.Concurrency 114 | } 115 | 116 | // Calculate an initial guess at the model parameters. 117 | init := []float64{0.1, 0.01, floats.Max(xn)} 118 | 119 | // Calculate the residuals of a possible model. 120 | f := func(dst, x []float64) { 121 | model := Model{Sigma: x[0], Kappa: x[1], Lambda: x[2]} 122 | 123 | for i, v := range measurements { 124 | dst[i] = v.Throughput - model.ThroughputAtConcurrency(v.Concurrency) 125 | } 126 | } 127 | j := lm.NumJac{Func: f} 128 | 129 | // Formulate an LM problem. 130 | p := lm.LMProblem{ 131 | Dim: 3, // Three parameters in the model. 132 | Size: len(measurements), // Use all measurements to calculate residuals. 133 | Func: f, // Reduce the residuals of model predictions to observations. 134 | Jac: j.Jac, // Approximate the Jacobian by finite differences. 135 | InitParams: init, // Use our initial guesses at parameters. 136 | Tau: 1e-6, // Need a non-zero initial damping factor. 137 | Eps1: 1e-8, // Small but non-zero values here prevent singular matrices. 138 | Eps2: 1e-8, 139 | } 140 | 141 | // Calculate the model parameters. 142 | results, err := lm.LM(p, nil) 143 | if err != nil { 144 | return nil, fmt.Errorf("unable to build model: %w", err) 145 | } 146 | 147 | // Return the model. 148 | return &Model{ 149 | Sigma: results.X[0], 150 | Kappa: results.X[1], 151 | Lambda: results.X[2], 152 | }, nil 153 | } 154 | 155 | const ( 156 | // minMeasurement is the smallest number of measurements from which a useful model can be 157 | // created. 158 | minMeasurements = 6 159 | ) 160 | 161 | // ErrInsufficientMeasurements is returned when fewer than 6 measurements were provided. 162 | var ErrInsufficientMeasurements = fmt.Errorf("usl: need at least %d measurements", minMeasurements) 163 | -------------------------------------------------------------------------------- /model_test.go: -------------------------------------------------------------------------------- 1 | package usl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/codahale/gubbins/assert" 7 | "github.com/google/go-cmp/cmp/cmpopts" 8 | ) 9 | 10 | //nolint:gochecknoglobals // fine in tests 11 | var epsilon = cmpopts.EquateApprox(0.00001, 0) 12 | 13 | func TestModel_Kappa(t *testing.T) { 14 | t.Parallel() 15 | 16 | m := build(t) 17 | 18 | assert.Equal(t, "Kappa", 7.690945e-4, m.Kappa, epsilon) 19 | } 20 | 21 | func TestModel_Sigma(t *testing.T) { 22 | t.Parallel() 23 | 24 | m := build(t) 25 | 26 | assert.Equal(t, "Sigma", 0.02671591, m.Sigma, epsilon) 27 | } 28 | 29 | func TestModel_Lambda(t *testing.T) { 30 | t.Parallel() 31 | 32 | m := build(t) 33 | 34 | assert.Equal(t, "Lambda", 995.6486, m.Lambda, epsilon) 35 | } 36 | 37 | func TestModel_MaxConcurrency(t *testing.T) { 38 | t.Parallel() 39 | 40 | m := build(t) 41 | 42 | assert.Equal(t, "MaxConcurrency", 35.0, m.MaxConcurrency(), epsilon) 43 | } 44 | 45 | func TestModel_MaxThroughput(t *testing.T) { 46 | t.Parallel() 47 | 48 | m := build(t) 49 | 50 | assert.Equal(t, "MaxThroughput", 12341.745415132369, m.MaxThroughput(), epsilon) 51 | } 52 | 53 | func TestModel_CoherencyConstrained(t *testing.T) { 54 | t.Parallel() 55 | 56 | m := build(t) 57 | 58 | assert.Equal(t, "CoherencyConstrained", false, m.CoherencyConstrained()) 59 | } 60 | 61 | func TestModel_ContentionConstrained(t *testing.T) { 62 | t.Parallel() 63 | 64 | m := build(t) 65 | 66 | assert.Equal(t, "ContentionConstrained", true, m.ContentionConstrained()) 67 | } 68 | 69 | func TestModel_LatencyAtConcurrency(t *testing.T) { 70 | t.Parallel() 71 | 72 | m := build(t) 73 | 74 | assert.Equal(t, "R(N=1)", 0.0010043702853760425, m.LatencyAtConcurrency(1), epsilon) 75 | assert.Equal(t, "R(N=20)", 0.0018077244276309343, m.LatencyAtConcurrency(20), epsilon) 76 | assert.Equal(t, "R(N=35)", 0.0028359035794958197, m.LatencyAtConcurrency(35), epsilon) 77 | } 78 | 79 | func TestModel_ThroughputAtConcurrency(t *testing.T) { 80 | t.Parallel() 81 | 82 | m := build(t) 83 | 84 | assert.Equal(t, "X(N=1)", 995.648772003358, m.ThroughputAtConcurrency(1), epsilon) 85 | assert.Equal(t, "X(N=20)", 11063.63312570436, m.ThroughputAtConcurrency(20), epsilon) 86 | assert.Equal(t, "X(N=35)", 12341.745655201905, m.ThroughputAtConcurrency(35), epsilon) 87 | } 88 | 89 | func TestModel_ConcurrencyAtThroughput(t *testing.T) { 90 | t.Parallel() 91 | 92 | m := build(t) 93 | 94 | assert.Equal(t, "N(X=955)", 0.9580998829620233, m.ConcurrencyAtThroughput(955), epsilon) 95 | assert.Equal(t, "N(X=11048)", 15.350435172752203, m.ConcurrencyAtThroughput(11048), epsilon) 96 | assert.Equal(t, "N(X=12201)", 17.73220762025387, m.ConcurrencyAtThroughput(12201), epsilon) 97 | } 98 | 99 | func TestModel_ThroughputAtLatency(t *testing.T) { 100 | t.Parallel() 101 | 102 | m := &Model{Sigma: 0.06, Kappa: 0.06, Lambda: 40} 103 | 104 | assert.Equal(t, "X(R=0.03)", 69.38886664887109, m.ThroughputAtLatency(0.03), epsilon) 105 | assert.Equal(t, "X(R=0.04", 82.91561975888501, m.ThroughputAtLatency(0.04), epsilon) 106 | assert.Equal(t, "X(R=0.05)", 84.06346808612327, m.ThroughputAtLatency(0.05), epsilon) 107 | } 108 | 109 | func TestModel_LatencyAtThroughput(t *testing.T) { 110 | t.Parallel() 111 | 112 | m := &Model{Sigma: 0.06, Kappa: 0.06, Lambda: 40} 113 | 114 | assert.Equal(t, "R(N=400)", 0.05875, m.LatencyAtThroughput(400), epsilon) 115 | assert.Equal(t, "R(N=500", 0.094, m.LatencyAtThroughput(500), epsilon) 116 | assert.Equal(t, "R(N=600)", 0.235, m.LatencyAtThroughput(600), epsilon) 117 | } 118 | 119 | func TestModel_ConcurrencyAtLatency(t *testing.T) { 120 | t.Parallel() 121 | 122 | m, err := Build(measurements[:10]) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | assert.Equal(t, "N(R=0.0012)", 7.230628979597649, m.ConcurrencyAtLatency(0.0012), epsilon) 128 | assert.Equal(t, "N(R=0.0016)", 20.25106409917121, m.ConcurrencyAtLatency(0.0016), epsilon) 129 | assert.Equal(t, "N(R=0.0020)", 29.88889360938781, m.ConcurrencyAtLatency(0.0020), epsilon) 130 | } 131 | 132 | func TestModel_Limitless(t *testing.T) { 133 | t.Parallel() 134 | 135 | m := &Model{Sigma: 1, Kappa: 0, Lambda: 40} 136 | 137 | assert.Equal(t, "Limitless", true, m.Limitless()) 138 | 139 | m = build(t) 140 | 141 | assert.Equal(t, "Limitless", false, m.Limitless()) 142 | } 143 | 144 | func TestModel_String(t *testing.T) { 145 | t.Parallel() 146 | 147 | m := &Model{Sigma: 1, Kappa: 2, Lambda: 3} 148 | 149 | assert.Equal(t, "String", "Model{σ=1,κ=2,λ=3}", m.String()) 150 | } 151 | 152 | func BenchmarkBuild(b *testing.B) { 153 | for i := 0; i < b.N; i++ { 154 | build(b) 155 | } 156 | } 157 | 158 | //nolint:gochecknoglobals // fine in tests 159 | var measurements = []Measurement{ 160 | ConcurrencyAndThroughput(1, 955.16), 161 | ConcurrencyAndThroughput(2, 1878.91), 162 | ConcurrencyAndThroughput(3, 2688.01), 163 | ConcurrencyAndThroughput(4, 3548.68), 164 | ConcurrencyAndThroughput(5, 4315.54), 165 | ConcurrencyAndThroughput(6, 5130.43), 166 | ConcurrencyAndThroughput(7, 5931.37), 167 | ConcurrencyAndThroughput(8, 6531.08), 168 | ConcurrencyAndThroughput(9, 7219.8), 169 | ConcurrencyAndThroughput(10, 7867.61), 170 | ConcurrencyAndThroughput(11, 8278.71), 171 | ConcurrencyAndThroughput(12, 8646.7), 172 | ConcurrencyAndThroughput(13, 9047.84), 173 | ConcurrencyAndThroughput(14, 9426.55), 174 | ConcurrencyAndThroughput(15, 9645.37), 175 | ConcurrencyAndThroughput(16, 9897.24), 176 | ConcurrencyAndThroughput(17, 10097.6), 177 | ConcurrencyAndThroughput(18, 10240.5), 178 | ConcurrencyAndThroughput(19, 10532.39), 179 | ConcurrencyAndThroughput(20, 10798.52), 180 | ConcurrencyAndThroughput(21, 11151.43), 181 | ConcurrencyAndThroughput(22, 11518.63), 182 | ConcurrencyAndThroughput(23, 11806), 183 | ConcurrencyAndThroughput(24, 12089.37), 184 | ConcurrencyAndThroughput(25, 12075.41), 185 | ConcurrencyAndThroughput(26, 12177.29), 186 | ConcurrencyAndThroughput(27, 12211.41), 187 | ConcurrencyAndThroughput(28, 12158.93), 188 | ConcurrencyAndThroughput(29, 12155.27), 189 | ConcurrencyAndThroughput(30, 12118.04), 190 | ConcurrencyAndThroughput(31, 12140.4), 191 | ConcurrencyAndThroughput(32, 12074.39), 192 | } 193 | 194 | func build(tb testing.TB) *Model { 195 | tb.Helper() 196 | 197 | m, err := Build(measurements) 198 | if err != nil { 199 | tb.Fatal(err) 200 | } 201 | 202 | return m 203 | } 204 | --------------------------------------------------------------------------------