nim-chronos/tests/testutils.nim

265 lines
8.5 KiB
Nim
Raw Normal View History

# 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)
exception tracking (#166) * exception tracking This PR adds minimal exception tracking to chronos, moving the goalpost one step further. In particular, it becomes invalid to raise exceptions from `callSoon` callbacks: this is critical for writing correct error handling because there's no reasonable way that a user of chronos can possibly _reason_ about exceptions coming out of there: the event loop will be in an indeterminite state when the loop is executing an _random_ callback. As expected, there are several issues in the error handling of chronos: in particular, it will end up in an inconsistent internal state whenever the selector loop operations fail, because the internal state update functions are not written in an exception-safe way. This PR turns this into a Defect, which probably is not the optimal way of handling things - expect more work to be done here. Some API have no way of reporting back errors to callers - for example, when something fails in the accept loop, there's not much it can do, and no way to report it back to the user of the API - this has been fixed with the new accept flow - the old one should be deprecated. Finally, there is information loss in the API: in composite operations like `poll` and `waitFor` there's no way to differentiate internal errors from user-level errors originating from callbacks. * store `CatchableError` in future * annotate proc's with correct raises information * `selectors2` to avoid non-CatchableError IOSelectorsException * `$` should never raise * remove unnecessary gcsafe annotations * fix exceptions leaking out of timer waits * fix some imports * functions must signal raising the union of all exceptions across all platforms to enable cross-platform code * switch to unittest2 * add `selectors2` which supercedes the std library version and fixes several exception handling issues in there * fixes * docs, platform-independent eh specifiers for some functions * add feature flag for strict exception mode also bump version to 3.0.0 - _most_ existing code should be compatible with this version of exception handling but some things might need fixing - callbacks, existing raises specifications etc. * fix AsyncCheck for non-void T
2021-03-24 10:08:33 +01:00
import unittest2
import ../chronos, ../chronos/config
{.used.}
2023-10-26 13:50:03 -07:00
when chronosFuturesInstrumentation:
import std/[tables, os, options, hashes]
import ../chronos/timer
2023-11-01 17:05:55 -07:00
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()
2023-10-26 13:50:03 -07:00
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:
2023-11-01 17:05:55 -07:00
proc setFutureCreate(fut: FutureBase) {.nimcall, gcsafe, raises: [].} =
2023-10-26 13:50:03 -07:00
## used for setting the duration
2023-11-01 17:05:55 -07:00
{.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: [].} =
2023-10-26 13:50:03 -07:00
## used for setting the duration
2023-11-01 17:05:55 -07:00
{.cast(gcsafe).}:
let loc = fut.internalLocation[Create]
assert futureDurations.hasKey(fut.id)
futureDurations.withValue(fut.id, metric):
2023-10-26 13:50:03 -07:00
let ts = Moment.now()
2023-11-01 17:05:55 -07:00
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: [].} =
2023-10-26 13:50:03 -07:00
## used for setting the duration
2023-11-01 17:05:55 -07:00
{.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
2023-10-26 13:50:03 -07:00
onFutureCreate =
2023-11-01 17:05:55 -07:00
proc (f: FutureBase) {.nimcall, gcsafe, raises: [].} =
2023-10-26 13:50:03 -07:00
f.setFutureCreate()
onFutureRunning =
2023-11-01 17:05:55 -07:00
proc (f: FutureBase) {.nimcall, gcsafe, raises: [].} =
2023-10-26 13:50:03 -07:00
f.setFutureStart()
onFuturePause =
2023-11-01 17:05:55 -07:00
proc (f, child: FutureBase) {.nimcall, gcsafe, raises: [].} =
2023-10-26 13:50:03 -07:00
f.setFuturePause(child)
onFutureStop =
2023-11-01 17:05:55 -07:00
proc (f: FutureBase) {.nimcall, gcsafe, raises: [].} =
2023-10-26 13:50:03 -07:00
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":
2023-10-26 15:23:13 -07:00
echo "v: ", v
check v.totalExecTime >= 100.milliseconds()
2023-10-26 13:50:03 -07:00
check v.totalExecTime <= 180.milliseconds()
check v.totalRunTime >= 150.milliseconds()
2023-10-27 16:16:23 -07:00
check v.totalRunTime <= 240.milliseconds()
2023-10-26 13:50:03 -07:00
discard
echo ""
else:
skip()