diff --git a/chronos.nimble b/chronos.nimble index 5312f7d75..d64619163 100644 --- a/chronos.nimble +++ b/chronos.nimble @@ -80,6 +80,14 @@ task test_libbacktrace, "test with libbacktrace": if (NimMajor, NimMinor) > (1, 6): run args & " --mm:orc", "tests/testall" +task test_profiler, "test with profiler instrumentation": + var allArgs = @[ + "-d:release -d:chronosFutureId -d:chronosProfiling", + ] + + for args in allArgs: + run args, "tests/testall" + task docs, "Generate API documentation": exec "mdbook build docs" exec nimc & " doc " & "--git.url:https://github.com/status-im/nim-chronos --git.commit:master --outdir:docs/book/api --project chronos" diff --git a/chronos/config.nim b/chronos/config.nim index 26d110f1b..9aabaa35d 100644 --- a/chronos/config.nim +++ b/chronos/config.nim @@ -40,7 +40,12 @@ const chronosStackTrace* {.booldefine.}: bool = defined(chronosDebug) ## Include stack traces in futures for creation and completion points - chronosFutureId* {.booldefine.}: bool = defined(chronosDebug) + chronosProfiling* {.booldefine.} = defined(chronosProfiling) + ## Enable instrumentation callbacks which are called at + ## the start, pause, or end of a Future's lifetime. + ## Useful for implementing metrics or other instrumentation. + + chronosFutureId* {.booldefine.}: bool = defined(chronosDebug) or chronosProfiling ## Generate a unique `id` for every future - when disabled, the address of ## the future will be used instead diff --git a/chronos/futures.nim b/chronos/futures.nim index fd8dbfe70..f6171dcd7 100644 --- a/chronos/futures.nim +++ b/chronos/futures.nim @@ -111,6 +111,13 @@ when chronosFutureTracking: var futureList* {.threadvar.}: FutureList +when chronosProfiling: + type AsyncFutureState* {.pure.} = enum + Running, Paused + + var onBaseFutureEvent* {.threadvar.}: proc (fut: FutureBase, state: FutureState): void {.nimcall, gcsafe, raises: [].} + var onAsyncFutureEvent* {.threadvar.}: proc(fut: FutureBase, state: AsyncFutureState): void {.nimcall, gcsafe, raises: [].} + # Internal utilities - these are not part of the stable API proc internalInitFutureBase*(fut: FutureBase, loc: ptr SrcLoc, state: FutureState, flags: FutureFlags) = @@ -144,6 +151,10 @@ proc internalInitFutureBase*(fut: FutureBase, loc: ptr SrcLoc, futureList.head = fut futureList.count.inc() + when chronosProfiling: + if not isNil(onBaseFutureEvent): + onBaseFutureEvent(fut, state) + # Public API template init*[T](F: type Future[T], fromProc: static[string] = ""): Future[T] = ## Creates a new pending future. diff --git a/chronos/internal/asyncfutures.nim b/chronos/internal/asyncfutures.nim index 6c8f2bddb..248f2256e 100644 --- a/chronos/internal/asyncfutures.nim +++ b/chronos/internal/asyncfutures.nim @@ -181,6 +181,11 @@ proc finish(fut: FutureBase, state: FutureState) = # 1. `finish()` is a private procedure and `state` is under our control. # 2. `fut.state` is checked by `checkFinished()`. fut.internalState = state + + when chronosProfiling: + if not isNil(onBaseFutureEvent): + onBaseFutureEvent(fut, state) + fut.internalCancelcb = nil # release cancellation callback memory for item in fut.internalCallbacks.mitems(): if not(isNil(item.function)): @@ -365,6 +370,10 @@ proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.} = # # Every call to an `{.async.}` proc is redirected to call this function # instead with its original body captured in `fut.closure`. + when chronosProfiling: + if not isNil(onAsyncFutureEvent): + onAsyncFutureEvent(fut, Running) + while true: # Call closure to make progress on `fut` until it reaches `yield` (inside # `await` typically) or completes / fails / is cancelled @@ -382,6 +391,10 @@ proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.} = GC_ref(fut) next.addCallback(CallbackFunc(internalContinue), cast[pointer](fut)) + when chronosProfiling: + if not isNil(onAsyncFutureEvent): + onAsyncFutureEvent(fut, Paused) + # return here so that we don't remove the closure below return