nim-chronos/tests/testutils.nim
Jaremy Creechley d0ed08c4a3
non-procs
2023-11-01 17:05:55 -07:00

265 lines
8.5 KiB
Nim

# Chronos Test Suite
# (c) Copyright 2020-Present
# Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import unittest2
import ../chronos, ../chronos/config
{.used.}
when chronosFuturesInstrumentation:
import std/[tables, os, options, hashes]
import ../chronos/timer
type
FutureMetric = object
## Holds average timing information for a given closure
closureLoc*: ptr SrcLoc
created*: Moment
start*: Option[Moment]
duration*: Duration
blocks*: int
initDuration*: Duration
durationChildren*: Duration
CallbackMetric = object
totalExecTime*: Duration
totalWallTime*: Duration
totalRunTime*: Duration
minSingleTime*: Duration
maxSingleTime*: Duration
count*: int64
var
futureDurations: Table[uint, FutureMetric]
callbackDurations: Table[ptr SrcLoc, CallbackMetric]
suite "Asynchronous utilities test suite":
when chronosFutureTracking:
proc getCount(): uint =
# This procedure counts number of Future[T] in double-linked list via list
# iteration.
var res = 0'u
for item in pendingFutures():
inc(res)
res
test "Future clean and leaks test":
when chronosFutureTracking:
if pendingFuturesCount(WithoutCompleted) == 0'u:
if pendingFuturesCount(OnlyCompleted) > 0'u:
poll()
check pendingFuturesCount() == 0'u
else:
echo dumpPendingFutures()
check false
else:
skip()
test "FutureList basics test":
when chronosFutureTracking:
var fut1 = newFuture[void]()
check:
getCount() == 1'u
pendingFuturesCount() == 1'u
var fut2 = newFuture[void]()
check:
getCount() == 2'u
pendingFuturesCount() == 2'u
var fut3 = newFuture[void]()
check:
getCount() == 3'u
pendingFuturesCount() == 3'u
fut1.complete()
poll()
check:
getCount() == 2'u
pendingFuturesCount() == 2'u
fut2.fail(newException(ValueError, ""))
poll()
check:
getCount() == 1'u
pendingFuturesCount() == 1'u
fut3.cancel()
poll()
check:
getCount() == 0'u
pendingFuturesCount() == 0'u
else:
skip()
test "FutureList async procedure test":
when chronosFutureTracking:
proc simpleProc() {.async.} =
await sleepAsync(10.milliseconds)
var fut = simpleProc()
check:
getCount() == 2'u
pendingFuturesCount() == 2'u
waitFor fut
check:
getCount() == 1'u
pendingFuturesCount() == 1'u
poll()
check:
getCount() == 0'u
pendingFuturesCount() == 0'u
else:
skip()
test "check empty futures instrumentation runs":
when chronosFuturesInstrumentation:
proc simpleAsyncChild() {.async.} =
echo "child sleep..."
os.sleep(25)
proc simpleAsync1() {.async.} =
for i in 0..1:
await sleepAsync(40.milliseconds)
await simpleAsyncChild()
echo "sleep..."
os.sleep(50)
waitFor(simpleAsync1())
check true
test "Example of using Future hooks to gather metrics":
when chronosFuturesInstrumentation:
proc setFutureCreate(fut: FutureBase) {.nimcall, gcsafe, raises: [].} =
## used for setting the duration
{.cast(gcsafe).}:
let loc = fut.internalLocation[Create]
futureDurations[fut.id] = FutureMetric()
futureDurations.withValue(fut.id, metric):
metric.created = Moment.now()
echo loc, "; future create "
proc setFutureStart(fut: FutureBase) {.nimcall, gcsafe, raises: [].} =
## used for setting the duration
{.cast(gcsafe).}:
let loc = fut.internalLocation[Create]
assert futureDurations.hasKey(fut.id)
futureDurations.withValue(fut.id, metric):
let ts = Moment.now()
metric.start = some ts
metric.blocks.inc()
echo loc, "; future start: ", metric.initDuration
proc setFuturePause(fut, child: FutureBase) {.nimcall, gcsafe, raises: [].} =
## used for setting the duration
{.cast(gcsafe).}:
let loc = fut.internalLocation[Create]
let childLoc = if child.isNil: nil else: child.internalLocation[Create]
var durationChildren = ZeroDuration
var initDurationChildren = ZeroDuration
if childLoc != nil:
futureDurations.withValue(child.id, metric):
durationChildren = metric.duration
initDurationChildren = metric.initDuration
assert futureDurations.hasKey(fut.id)
futureDurations.withValue(fut.id, metric):
if metric.start.isSome:
let ts = Moment.now()
metric.duration += ts - metric.start.get()
metric.duration -= initDurationChildren
if metric.blocks == 1:
metric.initDuration = ts - metric.created # tricky,
# the first block of a child iterator also
# runs on the parents clock, so we track our first block
# time so any parents can get it
echo loc, "; child firstBlock time: ", initDurationChildren
metric.durationChildren += durationChildren
metric.start = none Moment
echo loc, "; future pause ", if childLoc.isNil: "" else: " child: " & $childLoc
proc setFutureDuration(fut: FutureBase) {.nimcall, gcsafe, raises: [].} =
## used for setting the duration
{.cast(gcsafe).}:
let loc = fut.internalLocation[Create]
# assert "set duration: " & $loc
var fm: FutureMetric
# assert futureDurations.pop(fut.id, fm)
futureDurations.withValue(fut.id, metric):
fm = metric[]
discard callbackDurations.hasKeyOrPut(loc, CallbackMetric(minSingleTime: InfiniteDuration))
callbackDurations.withValue(loc, metric):
echo loc, " set duration: ", callbackDurations.hasKey(loc)
metric.totalExecTime += fm.duration
metric.totalWallTime += Moment.now() - fm.created
metric.totalRunTime += metric.totalExecTime + fm.durationChildren
echo loc, " child duration: ", fm.durationChildren
metric.count.inc
metric.minSingleTime = min(metric.minSingleTime, fm.duration)
metric.maxSingleTime = max(metric.maxSingleTime, fm.duration)
# handle overflow
if metric.count == metric.count.typeof.high:
metric.totalExecTime = ZeroDuration
metric.count = 0
onFutureCreate =
proc (f: FutureBase) {.nimcall, gcsafe, raises: [].} =
f.setFutureCreate()
onFutureRunning =
proc (f: FutureBase) {.nimcall, gcsafe, raises: [].} =
f.setFutureStart()
onFuturePause =
proc (f, child: FutureBase) {.nimcall, gcsafe, raises: [].} =
f.setFuturePause(child)
onFutureStop =
proc (f: FutureBase) {.nimcall, gcsafe, raises: [].} =
f.setFuturePause(nil)
f.setFutureDuration()
proc simpleAsyncChild() {.async.} =
echo "child sleep..."
os.sleep(25)
proc simpleAsync1() {.async.} =
for i in 0..1:
await sleepAsync(40.milliseconds)
await simpleAsyncChild()
echo "sleep..."
os.sleep(50)
waitFor(simpleAsync1())
let metrics = callbackDurations
echo "\n=== metrics ==="
echo "execTime:\ttime to execute non-async portions of async proc"
echo "runTime:\texecution time + execution time of children"
echo "wallTime:\twall time elapsed for future's lifetime"
for (k,v) in metrics.pairs():
let count = v.count
if count > 0:
echo ""
echo "metric: ", $k
echo "count: ", count
echo "avg execTime:\t", v.totalExecTime div count, "\ttotal: ", v.totalExecTime
echo "avg wallTime:\t", v.totalWallTime div count, "\ttotal: ", v.totalWallTime
echo "avg runTime:\t", v.totalRunTime div count, "\ttotal: ", v.totalRunTime
if k.procedure == "simpleAsync1":
echo "v: ", v
check v.totalExecTime >= 100.milliseconds()
check v.totalExecTime <= 180.milliseconds()
check v.totalRunTime >= 150.milliseconds()
check v.totalRunTime <= 240.milliseconds()
discard
echo ""
else:
skip()