core: Add benchmarking tools

This commit is contained in:
Alex Jbanca 2023-03-08 15:55:26 +02:00 committed by Alex Jbanca
parent d8473f4d4f
commit a7943e79af
1 changed files with 169 additions and 2 deletions

View File

@ -1,5 +1,4 @@
import strutils, times import times, macros, strutils, locks, algorithm
# benchmark measure execution time of block of code. # benchmark measure execution time of block of code.
# usage: # usage:
# import os, strutils, times # import os, strutils, times
@ -15,3 +14,171 @@ template benchmark*(benchmarkName: string, code: untyped) =
let elapsed = epochTime() - t0 let elapsed = epochTime() - t0
let elapsedStr = elapsed.formatFloat(format = ffDecimal, precision = 10) let elapsedStr = elapsed.formatFloat(format = ffDecimal, precision = 10)
echo "CPU Time [", benchmarkName, "] ", elapsedStr, "s" echo "CPU Time [", benchmarkName, "] ", elapsedStr, "s"
# Thread safe benchmarking tools
# There are two ways to use this module:
# 1. Use the benchmark benchmarkProc macro to benchmark a block of code. It will automatically call registerDuration with the name of the procedure and the start and end time
# 2. Manually call registerDuration with name and the start and end time
# The output file is named procBenchmark.csv and is located in the current working directory. It will be created at app exit.
# The output is sorted by total time in descending order.
# Output format:
# Procedure, Number of runs, Total time in ms, Avg time in ms, Weight
# test*,1,100.00,100.00,50.00%
# test2*,1,100.00,100.00,50.00%
# usage:
# proc test() {.benchmarkProc.} =
# sleep(1)
# proc test2() {.benchmarkProc.} =
# sleep(1)
# proc test3() =
# let cpuTime = cpuTime()
# defer: registerDuration("custom test3", cpuTime, cpuTime())
# sleep(1)
type
ProcBenchmark* = object
numRuns*: int
totalTime*: float
procName*: cstring
ProcBenchmarkPtr* = ptr ProcBenchmark
SharedProcBenchmarkArr* = object
data: ptr UncheckedArray[ProcBenchmarkPtr]
len*: int
maxLen*: int
BenchmarkResult = tuple[procName: string, numRuns: int, totalTime: float, weight: float]
var
lock: Lock
benchmarkResults: ptr SharedProcBenchmarkArr
# Forward declarations
proc registerDuration*(name: string, startTime: float, endTime: float) {.gcsafe.}
proc quitCallback() {.noconv.}
proc initBenchmarking() {.gcsafe.}
proc deinitBenchmarking()
proc aggregateData(): seq[BenchmarkResult]
proc writeToFile(resultsToWrite: seq[BenchmarkResult])
proc resultCmp(x, y: BenchmarkResult): int
# End of forward declarations
addQuitProc(quitCallback)
macro benchmarkProc*(procDef: untyped): untyped =
procDef.expectKind(nnkProcDef)
let
procName = procDef[0].toStrLit
let
startingBenchmark = quote do:
let startTime = cpuTime()
defer: registerDuration(`procName`, startTime, cpuTime())
procDef.body.insert(0, startingBenchmark)
return procDef
proc registerDuration*(name: string, startTime: float, endTime: float) {.gcsafe.} =
if(benchmarkResults == nil):
initBenchmarking()
withLock lock:
var found = false
for i in 0 ..< benchmarkResults.len:
if benchmarkResults.data[i].procName == name.cstring:
benchmarkResults.data[i].numRuns += 1
benchmarkResults.data[i].totalTime += endTime - startTime
found = true
if found:
break
if not found:
if benchmarkResults.len == benchmarkResults.maxLen:
benchmarkResults.maxLen = benchmarkResults.maxLen * 2
benchmarkResults.data = cast[ptr UncheckedArray[ProcBenchmarkPtr]](reallocShared(benchmarkResults.data, sizeof(ProcBenchmark) * benchmarkResults.maxLen))
var newProcBenchmark = cast[ProcBenchmarkPtr](allocShared(sizeof(ProcBenchmark)))
var namePtr = cast[cstring](allocShared(name.len+1))
copyMem(namePtr, name.cstring, name.len)
namePtr[name.len] = '\0'
newProcBenchmark.procName = namePtr
newProcBenchmark.numRuns = 1
newProcBenchmark.totalTime = endTime - startTime
benchmarkResults.data[benchmarkResults.len] = newProcBenchmark
benchmarkResults.len += 1
proc quitCallback() {.noconv.} =
echo "Benchmarking quit callback"
var
results = aggregateData()
results.writeToFile()
deinitBenchmarking()
proc initBenchmarking() {.gcsafe.} =
echo "Benchmarking init"
initLock(lock)
withLock lock:
if benchmarkResults != nil:
return
benchmarkResults = cast[ptr SharedProcBenchmarkArr](allocShared(sizeof(SharedProcBenchmarkArr)))
benchmarkResults.len = 0
benchmarkResults.maxLen = 1000
benchmarkResults.data = cast[ptr UncheckedArray[ProcBenchmarkPtr]](allocShared(sizeof(ProcBenchmark) * benchmarkResults.maxLen))
proc deinitBenchmarking() =
echo "Benchmarking deinit"
withLock lock:
if benchmarkResults == nil:
return
for i in 0 ..< benchmarkResults.len:
deallocShared(benchmarkResults.data[i].procName)
deallocShared(benchmarkResults.data)
deallocShared(benchmarkResults)
benchmarkResults = nil
proc aggregateData(): seq[BenchmarkResult] =
result = @[]
var totalWeight = 0.0
withLock lock:
for i in 0 ..< benchmarkResults.len:
totalWeight += benchmarkResults.data[i].totalTime
result.add(($(benchmarkResults.data[i].procName), benchmarkResults.data[i].numRuns, benchmarkResults.data[i].totalTime, 0.0))
result.sort(resultCmp)
for i in 0 ..< result.len:
result[i].weight = result[i].totalTime / totalWeight
proc writeToFile(resultsToWrite: seq[BenchmarkResult]) =
echo "Benchmarking - write to file"
var file: File
try:
file = open("procBenchmark.csv", fmWrite)
except:
echo "Could not open file procBenchmark.txt for writing"
return
file.writeLine("Procedure, Number of runs, Total time in ms, Avg time in ms, Weight")
for entry in resultsToWrite:
file.writeLine(entry.procName &
"," & $entry.numRuns &
"," & $(entry.totalTime * 1000) &
"," & $((entry.totalTime / entry.numRuns.float) * 1000) &
"," & $(entry.weight * 100) & "%")
file.close()
proc resultCmp(x, y: BenchmarkResult): int =
cmp(y.totalTime, x.totalTime)