├── .github └── workflows │ ├── build.yml │ └── docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── benchy.nimble ├── src └── benchy.nim └── tests ├── config.nims └── test.nim /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | nim-version: ['1.2.2', '2.0.2'] 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | 22 | - uses: actions/checkout@v3 23 | with: 24 | path: "${{ github.event.repository.name }}" 25 | 26 | - name: Install Choosenim 27 | run: | 28 | curl -O https://nim-lang.org/choosenim/init.sh 29 | sh init.sh -y 30 | 31 | - name: Add Nim to PATH (linux) 32 | if: runner.os == 'Linux' 33 | run: echo "/home/runner/.nimble/bin" >> $GITHUB_PATH 34 | 35 | - name: Add Nim to PATH (windows) 36 | if: runner.os == 'Windows' 37 | run: | 38 | echo "C:\Users\runneradmin\.nimble\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 39 | shell: pwsh 40 | 41 | - name: Install Atlas 42 | run: | 43 | git clone https://github.com/nim-lang/atlas.git 44 | cd atlas 45 | git checkout cbba9fa77fa837931bf3c58e20c1f8cb15a22919 46 | nim c src/atlas.nim 47 | 48 | - name: Add Atlas to PATH (linux) 49 | if: runner.os == 'Linux' 50 | run: mv atlas/src/atlas /home/runner/.nimble/bin 51 | 52 | - name: Add Atlas to PATH (windows) 53 | if: runner.os == 'Windows' 54 | run: mv atlas/src/atlas.exe C:\Users\runneradmin\.nimble\bin 55 | shell: pwsh 56 | 57 | - name: Init Atlas 58 | run: atlas init 59 | 60 | - name: Install this Commit 61 | run: atlas use file://${{ github.event.repository.name }} 62 | 63 | - name: List nim.cfg 64 | run: cat nim.cfg 65 | 66 | - name: Install Version 67 | run: choosenim ${{ matrix.nim-version }} 68 | 69 | - name: Run tests 70 | run: | 71 | cd "${{ github.event.repository.name }}" 72 | # List test files here 73 | 74 | nim c -r tests/test.nim 75 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | nim-version: 'stable' 8 | nim-src: src/${{ github.event.repository.name }}.nim 9 | deploy-dir: .gh-pages 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: jiro4989/setup-nim-action@v1 16 | with: 17 | nim-version: ${{ env.nim-version }} 18 | - run: nimble install -Y 19 | - run: nimble doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} 20 | - name: "Copy to index.html" 21 | run: cp ${{ env.deploy-dir }}/${{ github.event.repository.name }}.html ${{ env.deploy-dir }}/index.html 22 | - name: Deploy documents 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ${{ env.deploy-dir }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files with no extention: 2 | * 3 | !*/ 4 | !*.* 5 | 6 | # normal ignores: 7 | *.exe 8 | nimcache 9 | *.pdb 10 | *.ilk 11 | .* 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Andre von Houck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Benchy 2 | 3 | * `atlas use benchy` 4 | * `nimble install benchy` 5 | 6 | ![Github Actions](https://github.com/treeform/benchy/workflows/Github%20Actions/badge.svg) 7 | 8 | [API reference](https://treeform.github.io/benchy) 9 | 10 | This library has no dependencies other than the Nim standard library. 11 | 12 | ## About 13 | 14 | Simple benchmarking to time your code. Just put your code in a `timeIt` block. Don't forgot to run with `-d:release`. 15 | 16 | ```nim 17 | import benchy, std/os, std/random 18 | 19 | timeIt "sleep 1ms": 20 | sleep(1) 21 | 22 | timeIt "sleep 200ms": 23 | sleep(200) 24 | 25 | timeIt "sleep random": 26 | sleep(rand(0 .. 150)) 27 | 28 | timeIt "number counter": 29 | var s = 0 30 | for i in 0 .. 1_000_000: 31 | s += s 32 | 33 | timeIt "string append": 34 | var s = "?" 35 | for i in 0 .. 26: 36 | s.add(s) 37 | ``` 38 | 39 | It will run the `timeIt` block at least 10 times but possibly more to figure out the standard deviation. It will keep running it until things look like they stabilized. It will stop after 60s though. 40 | 41 | ``` 42 | name ............................... min time avg time std dv runs 43 | sleep 1ms .......................... 1.016 ms 1.993 ms ±0.032 x1000 44 | sleep 200ms ...................... 200.403 ms 200.463 ms ±0.022 x25 45 | sleep random ....................... 5.959 ms 75.490 ms ±44.856 x67 46 | number counter ..................... 2.680 ms 2.747 ms ±0.052 x1000 47 | string append ..................... 36.322 ms 37.796 ms ±1.771 x127 48 | ``` 49 | -------------------------------------------------------------------------------- /benchy.nimble: -------------------------------------------------------------------------------- 1 | version = "0.0.1" 2 | author = "Andre von Houck" 3 | description = "Benchmarking" 4 | license = "MIT" 5 | 6 | srcDir = "src" 7 | 8 | requires "nim >= 1.2.2" 9 | -------------------------------------------------------------------------------- /src/benchy.nim: -------------------------------------------------------------------------------- 1 | import std/monotimes, strformat, math, strutils 2 | 3 | when defined(benchyAffinty): 4 | when defined(windows): 5 | proc GetCurrentProcess(): int {.stdcall, dynlib: "kernel32", importc.} 6 | proc SetProcessAffinityMask(handle: int, mask: uint64): cint 7 | {.stdcall, dynlib: "kernel32", importc.} 8 | 9 | const REALTIME_PRIORITY_CLASS = 0x00000100 10 | proc SetPriorityClass(handle: int, class: uint32): cint 11 | {.stdcall, dynlib: "kernel32", importc.} 12 | 13 | discard SetProcessAffinityMask( 14 | GetCurrentProcess(), 15 | 0b0000_0010 16 | ) 17 | discard SetPriorityClass( 18 | GetCurrentProcess(), 19 | REALTIME_PRIORITY_CLASS 20 | ) 21 | # TODO linux/mac 22 | 23 | proc nowMs(): float64 = 24 | ## Gets current milliseconds. 25 | getMonoTime().ticks.float64 / 1000000.0 26 | 27 | proc total(s: seq[float64]): float64 = 28 | ## Computes total of a sequence. 29 | for v in s: 30 | result += v.float 31 | 32 | proc min(s: seq[float64]): float64 = 33 | ## Computes mean (average) of a sequence. 34 | result = s[0].float 35 | for i in 1..s.high: 36 | result = min(s[i].float, result) 37 | 38 | proc mean(s: seq[float64]): float64 = 39 | ## Computes mean (average) of a sequence. 40 | if s.len == 0: return NaN 41 | s.total / s.len.float 42 | 43 | proc median(s: seq[float64]): float64 = 44 | ## Gets median (middle number) of a sequence. 45 | if s.len == 0: return NaN 46 | s[s.len div 2] 47 | 48 | proc variance(s: seq[float64]): float64 = 49 | ## Computes the sample variance of a sequence. 50 | if s.len <= 1: 51 | return 52 | let a = s.mean() 53 | for v in s: 54 | result += (v.float - a) ^ 2 55 | result /= (s.len.float - 1) 56 | 57 | proc stdDev(s: seq[float64]): float64 = 58 | ## Computes the sample standard deviation of a sequence. 59 | sqrt(s.variance) 60 | 61 | proc removeOutliers(s: var seq[float64], p = 3.0) = 62 | ## Remove numbers that are above p standard deviation. 63 | let avg = mean(s) 64 | let std = stdDev(s) 65 | var i = 0 66 | while i < s.len: 67 | if abs(s[i] - avg) > std * p: 68 | s.delete(i) 69 | else: 70 | inc i 71 | 72 | proc perBetween(s: seq[float64], a, b: float64): float64 = 73 | var count: int 74 | for n in s: 75 | if n >= a and n < b: 76 | inc count 77 | return count.float / s.len.float 78 | 79 | const brailleArr = [ 80 | ["⠀", "⢀", "⢠", "⢰", "⢸"], 81 | ["⡀", "⣀", "⣠", "⣰", "⣸"], 82 | ["⡄", "⣄", "⣤", "⣴", "⣴"], 83 | ["⡆", "⣆", "⣦", "⣶", "⣾"], 84 | ["⡇", "⣇", "⣧", "⣷", "⣿"], 85 | ] 86 | 87 | proc histogram(s: seq[float64]): string = 88 | let 89 | avg = mean(s) 90 | std = stdDev(s) 91 | cell = std * 0.2 92 | bucketRanges = [int.low, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, int.high] 93 | var 94 | buckets: seq[float64] 95 | for i in 0 ..< bucketRanges.len - 1: 96 | let 97 | a = avg - cell * 9 + cell * bucketRanges[i].float64 98 | b = avg - cell * 9 + cell * bucketRanges[i + 1].float64 99 | buckets.add perBetween(s, a, b) 100 | 101 | let maxVal = max(buckets) 102 | var 103 | intBuckets: seq[int] 104 | for b in buckets.mitems: 105 | intBuckets.add (b / maxVal * 4).ceil.int 106 | 107 | var brailleChart = "" 108 | for i in 0 ..< intBuckets.len div 2: 109 | #echo intBuckets[i]#, ", ", intBuckets[i*2 + 1] 110 | brailleChart.add brailleArr[intBuckets[i*2]][intBuckets[i*2+1]] 111 | return brailleChart 112 | 113 | var shownHeader = false # Only show the header once. 114 | 115 | template keep*(value: untyped) = 116 | discard 117 | 118 | template timeIt*(tag: string, iterations: untyped, body: untyped) = 119 | ## Template to time the block of code. 120 | if not shownHeader: 121 | shownHeader = true 122 | var header = " min time avg time std dv runs " 123 | when defined(benchyHistogram): 124 | header.add "histogram " 125 | header.add "name" 126 | echo header 127 | var 128 | num = 0 129 | minTime: float64 = float64.high 130 | lastMinCount: int 131 | total: float64 132 | deltas: seq[float64] 133 | 134 | block: 135 | proc test() {.gensym.} = 136 | body 137 | 138 | when defined(benchyExtra): 139 | # warm up 140 | for i in 0 ..< 15: 141 | test() 142 | 143 | while true: 144 | inc num 145 | let start = nowMs() 146 | 147 | test() 148 | 149 | let finish = nowMs() 150 | 151 | let delta = finish - start 152 | total += delta 153 | deltas.add(delta) 154 | 155 | when iterations != 0: 156 | if num >= iterations: 157 | break 158 | elif defined(benchyMinFinder): 159 | if minTime > delta: 160 | minTime = delta 161 | lastMinCount = 0 162 | inc lastMinCount 163 | if lastMinCount > 1000: 164 | break 165 | if total > 30_000.0: 166 | break 167 | else: 168 | if total > 5_000.0 or num >= 1000: 169 | break 170 | 171 | let minDelta = min(deltas) 172 | removeOutliers(deltas) 173 | let avgDelta = mean(deltas) 174 | let stdDev = stdDev(deltas) 175 | let median = median(deltas) 176 | 177 | var 178 | m, s, d: string 179 | formatValue(m, minDelta, "0.3f") 180 | formatValue(s, avgDelta, "0.3f") 181 | formatValue(d, stdDev, "0.3f") 182 | var row = align(m, 8) & " ms " & align(s, 8) & " ms " & align("±" & d, 8) & " " & align("x" & $num, 5) & " " 183 | when defined(benchyHistogram): 184 | row.add histogram(deltas) & " " 185 | row.add tag 186 | echo row 187 | 188 | template timeIt*(tag: string, body: untyped) = 189 | ## Template to time block of code. 190 | timeIt(tag, 0, body) 191 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | --path:"../src" 2 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | ## Put your tests here. 2 | 3 | import benchy, os, random 4 | 5 | timeIt "sleep 1ms": 6 | sleep(1) 7 | 8 | timeIt "sleep 200ms": 9 | sleep(200) 10 | 11 | timeIt "sleep random": 12 | sleep(rand(0 .. 150)) 13 | 14 | timeIt "number counter": 15 | var s = 0 16 | for i in 0 .. 1_000_000: 17 | s += s 18 | 19 | timeIt "string append": 20 | var s = "?" 21 | for i in 0 .. 26: 22 | s.add(s) 23 | 24 | timeIt "sleep 1ms x5", 5: 25 | sleep(1) 26 | 27 | timeIt "sleep 200ms x5", 5: 28 | sleep(200) 29 | 30 | timeIt "sleep random x20", 20: 31 | sleep(rand(0 .. 150)) 32 | 33 | timeIt "number counter x20", 20: 34 | var s = 0 35 | for i in 0 .. 1_000_000: 36 | s += s 37 | 38 | timeIt "string append x20", 20: 39 | var s = "?" 40 | for i in 0 .. 26: 41 | s.add(s) 42 | 43 | import strutils 44 | 45 | proc isSpace(c: char): bool = 46 | result = c in Whitespace 47 | 48 | timeIt "isSpaceAscii", 1000: 49 | for n in 0 .. 1000: 50 | for i in 1..255: 51 | let c = char(i) 52 | discard isSpaceAscii(c) 53 | 54 | timeIt "isSpace", 1000: 55 | for n in 0 .. 1000: 56 | for i in 1..255: 57 | let c = char(i) 58 | discard isSpace(c) 59 | 60 | proc test() = 61 | # See https://github.com/treeform/benchy/pull/10 62 | discard 63 | timeIt "test function": 64 | test() 65 | --------------------------------------------------------------------------------