2020-07-06 09:33:13 +03:00
|
|
|
# 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
|
2023-03-31 07:35:04 +02:00
|
|
|
import ../chronos, ../chronos/config
|
2020-07-06 09:33:13 +03:00
|
|
|
|
2023-03-31 07:35:04 +02:00
|
|
|
{.used.}
|
2020-07-06 09:33:13 +03:00
|
|
|
|
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]
|
|
|
|
|
|
2020-07-06 09:33:13 +03:00
|
|
|
suite "Asynchronous utilities test suite":
|
2023-03-31 07:35:04 +02:00
|
|
|
when chronosFutureTracking:
|
2021-10-21 17:22:11 +03:00
|
|
|
proc getCount(): uint =
|
2020-07-08 19:48:01 +03:00
|
|
|
# This procedure counts number of Future[T] in double-linked list via list
|
|
|
|
|
# iteration.
|
2021-10-21 17:22:11 +03:00
|
|
|
var res = 0'u
|
2020-07-08 19:48:01 +03:00
|
|
|
for item in pendingFutures():
|
2021-10-21 17:22:11 +03:00
|
|
|
inc(res)
|
|
|
|
|
res
|
2020-07-06 09:33:13 +03:00
|
|
|
|
|
|
|
|
test "Future clean and leaks test":
|
2023-03-31 07:35:04 +02:00
|
|
|
when chronosFutureTracking:
|
2023-05-25 17:31:35 +02:00
|
|
|
if pendingFuturesCount(WithoutCompleted) == 0'u:
|
|
|
|
|
if pendingFuturesCount(OnlyCompleted) > 0'u:
|
2020-07-08 19:48:01 +03:00
|
|
|
poll()
|
2021-10-21 17:22:11 +03:00
|
|
|
check pendingFuturesCount() == 0'u
|
2020-07-08 19:48:01 +03:00
|
|
|
else:
|
|
|
|
|
echo dumpPendingFutures()
|
|
|
|
|
check false
|
2020-07-06 09:33:13 +03:00
|
|
|
else:
|
2020-07-08 19:48:01 +03:00
|
|
|
skip()
|
2020-07-06 09:33:13 +03:00
|
|
|
|
|
|
|
|
test "FutureList basics test":
|
2023-03-31 07:35:04 +02:00
|
|
|
when chronosFutureTracking:
|
2020-07-08 19:48:01 +03:00
|
|
|
var fut1 = newFuture[void]()
|
|
|
|
|
check:
|
2021-10-21 17:22:11 +03:00
|
|
|
getCount() == 1'u
|
|
|
|
|
pendingFuturesCount() == 1'u
|
2020-07-08 19:48:01 +03:00
|
|
|
var fut2 = newFuture[void]()
|
|
|
|
|
check:
|
2021-10-21 17:22:11 +03:00
|
|
|
getCount() == 2'u
|
|
|
|
|
pendingFuturesCount() == 2'u
|
2020-07-08 19:48:01 +03:00
|
|
|
var fut3 = newFuture[void]()
|
|
|
|
|
check:
|
2021-10-21 17:22:11 +03:00
|
|
|
getCount() == 3'u
|
|
|
|
|
pendingFuturesCount() == 3'u
|
2020-07-08 19:48:01 +03:00
|
|
|
fut1.complete()
|
|
|
|
|
poll()
|
|
|
|
|
check:
|
2021-10-21 17:22:11 +03:00
|
|
|
getCount() == 2'u
|
|
|
|
|
pendingFuturesCount() == 2'u
|
2020-07-08 19:48:01 +03:00
|
|
|
fut2.fail(newException(ValueError, ""))
|
|
|
|
|
poll()
|
|
|
|
|
check:
|
2021-10-21 17:22:11 +03:00
|
|
|
getCount() == 1'u
|
|
|
|
|
pendingFuturesCount() == 1'u
|
2020-07-08 19:48:01 +03:00
|
|
|
fut3.cancel()
|
|
|
|
|
poll()
|
|
|
|
|
check:
|
2021-10-21 17:46:06 +03:00
|
|
|
getCount() == 0'u
|
|
|
|
|
pendingFuturesCount() == 0'u
|
2020-07-08 19:48:01 +03:00
|
|
|
else:
|
|
|
|
|
skip()
|
2020-07-06 09:33:13 +03:00
|
|
|
|
|
|
|
|
test "FutureList async procedure test":
|
2023-03-31 07:35:04 +02:00
|
|
|
when chronosFutureTracking:
|
2020-07-08 19:48:01 +03:00
|
|
|
proc simpleProc() {.async.} =
|
|
|
|
|
await sleepAsync(10.milliseconds)
|
2020-07-06 09:33:13 +03:00
|
|
|
|
2020-07-08 19:48:01 +03:00
|
|
|
var fut = simpleProc()
|
|
|
|
|
check:
|
2021-10-21 17:46:06 +03:00
|
|
|
getCount() == 2'u
|
|
|
|
|
pendingFuturesCount() == 2'u
|
2020-07-06 09:33:13 +03:00
|
|
|
|
2020-07-08 19:48:01 +03:00
|
|
|
waitFor fut
|
|
|
|
|
check:
|
2021-10-21 17:46:06 +03:00
|
|
|
getCount() == 1'u
|
|
|
|
|
pendingFuturesCount() == 1'u
|
2020-07-06 09:33:13 +03:00
|
|
|
|
2020-07-08 19:48:01 +03:00
|
|
|
poll()
|
|
|
|
|
check:
|
2021-10-21 17:46:06 +03:00
|
|
|
getCount() == 0'u
|
|
|
|
|
pendingFuturesCount() == 0'u
|
2020-07-08 19:48:01 +03:00
|
|
|
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
|
2023-10-31 12:38:47 -07:00
|
|
|
check v.totalExecTime >= 100.milliseconds()
|
2023-10-26 13:50:03 -07:00
|
|
|
check v.totalExecTime <= 180.milliseconds()
|
|
|
|
|
|
2023-10-31 12:38:47 -07:00
|
|
|
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()
|