Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b4bb2a8
Remove yield points while unwinding {.async.} proc
etan-status Jun 23, 2026
5c1f769
Apply suggestion from @etan-status
etan-status Jun 23, 2026
a9a2746
Restore when jungle
etan-status Jun 23, 2026
9695d1d
Add FutureFlag.SyncContinuations and opt-in config flag
etan-status Jun 24, 2026
9fd38ca
Sync nimble.config patterns
etan-status Jun 24, 2026
188f0b2
Rename new test file
etan-status Jun 24, 2026
fd0b6c9
Resolve multiple waiters in FIFO registration order
etan-status Jun 24, 2026
15effb0
Update chronos/config.nim
etan-status Jun 25, 2026
de0f175
Extend to all continuations, not just await continuations
etan-status Jun 25, 2026
194c456
Merge branch 'master' into dev/etan/ae-returnstrain
etan-status Jun 25, 2026
0352e6e
Match existing skip style
etan-status Jun 25, 2026
216fb74
Delay waitFor continuation to include existing other callbacks
etan-status Jun 25, 2026
02b4b6a
Add combinator tests
etan-status Jun 25, 2026
ce44964
Should not change consumer code
etan-status Jun 25, 2026
c5995e7
Flag to treat non-`{.async.}` callbacks as continuations
etan-status Jun 25, 2026
1c7b440
Apply suggestion from @etan-status
etan-status Jun 25, 2026
324499b
Merge branch 'master' into dev/etan/ae-returnstrain
etan-status Jul 9, 2026
b0a4008
Merge commit '637d96d30cb06109b69c07b63b758c934ec17503' into dev/etan…
etan-status Jul 11, 2026
5671183
Merge branch 'master' into dev/etan/ae-returnstrain
etan-status Jul 22, 2026
efe7248
Merge branch 'master' into dev/etan/ae-returnstrain
etan-status Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ jobs:
test-command: |
nimble install -y libbacktrace
nimble test
nimble test_sync_continuations
nimble test_libbacktrace
nimble examples
7 changes: 7 additions & 0 deletions chronos.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ task test, "Run all tests":
if f.startsWith("bench_") and f.endsWith(".nim"):
build "", f[0..^5]

task test_sync_continuations, "Run all tests with sync continuations":
for args in testArguments:
# First run tests with `refc` memory manager.
run args & " --mm:refc -d:chronosSyncContinuations", "tests/testall"
if (NimMajor, NimMinor) >= (2, 2): # ORC on 2.0 is too broken to investigate
run args & " --mm:orc -d:chronosSyncContinuations", "tests/testall"

task test_v3_compat, "Run all tests in v3 compatibility mode":
for args in testArguments:
if (NimMajor, NimMinor) >= (2, 2):
Expand Down
7 changes: 7 additions & 0 deletions chronos/config.nim
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ const
##
## `Exception` handling may be removed in future chronos versions.

chronosSyncContinuations* {.booldefine.}: bool = false
## When enabled, `CallbackFlag.Continuation` callbacks of futures with
## `FutureFlag.SyncContinuations` are scheduled to run before processing
## other already queued events such as timer handlers and I/O.
##
## This feature is experimental and may be removed in future releases.

chronosStrictFutureAccess* {.booldefine.}: bool = defined(chronosPreviewV5)

chronosStackTrace* {.booldefine.}: bool = defined(chronosDebug)
Expand Down
19 changes: 17 additions & 2 deletions chronos/futures.nim
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ type
# TODO https://github.com/nim-lang/Nim/issues/25976
CallbackFunc* = proc (arg: pointer) {.gcsafe, raises: [].}

CallbackFlag* {.pure.} = enum
Continuation
## When set and attached to a future with `FutureFlag.SyncContinuations`,
## the callback is scheduled to run before processing other already
## queued events such as timer handlers and I/O.
##
## Only works when the `chronosSyncContinuations` config is enabled.

# Internal type, not part of API
InternalAsyncCallback* = object
function*: CallbackFunc
Expand All @@ -50,6 +58,13 @@ type
## If `cancelCallback` is not set and the future gets cancelled, a
## `Defect` will be raised.

SyncContinuations
## When set, `CallbackFlag.Continuation` callbacks are scheduled
## to run before processing other already queued events such as
## timer handlers and I/O.
##
## Only works when the `chronosSyncContinuations` config is enabled.

FutureFlags* = set[FutureFlag]

InternalFutureBase* = object of RootObj
Expand All @@ -58,13 +73,13 @@ type
# structure may change in the future (haha)

internalLocation*: array[LocationKind, ptr SrcLoc]
internalCallback*: InternalAsyncCallback
internalContinuation*, internalCallback*: InternalAsyncCallback
## The vast majority of futures track a single callback only (the one
## installed by `await`) - to avoid allocating a seq (which involves
## making a separate allocation with space for several callbacks), we keep
## a spot in each future for that first one - the seq below will stay
## empty until a second callback is added
internalCallbacks*: seq[InternalAsyncCallback]
internalContinuations*, internalCallbacks*: seq[InternalAsyncCallback]
internalCancelcb*: CallbackFunc
internalChild*: FutureBase
internalState*: FutureState
Expand Down
37 changes: 26 additions & 11 deletions chronos/internal/asyncengine.nim
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type
trackers*: Table[string, TrackerBase]
counters*: Table[string, TrackerCounter]
inEventLoop: bool
numImmediate: int

proc sentinelCallbackImpl(arg: pointer) {.gcsafe, noreturn.} =
raiseAssert "Sentinel callback MUST not be scheduled"
Expand Down Expand Up @@ -160,10 +161,15 @@ template processCallbacks(loop: untyped) =
when chronosStrictReentrancy:
# Process existing callbacks but not those that follow, to allow the network
# to regain control regularly
for _ in 0 ..< len(loop.callbacks):
loop.numImmediate = 0
var numRemaining = len(loop.callbacks)
while numRemaining > 0:
dec(numRemaining)
let callable = loop.callbacks.popFirst()
if not(isNil(callable.function)):
callable.function(callable.udata)
numRemaining += loop.numImmediate
loop.numImmediate = 0
else:
while true:
let callable = loop.callbacks.popFirst() # len must be > 0 due to sentinel
Expand Down Expand Up @@ -1186,20 +1192,29 @@ proc removeTimer*(at: uint64, cb: CallbackFunc, udata: pointer = nil) {.
inline, deprecated: "Use removeTimer(Duration, cb, udata)".} =
removeTimer(Moment.init(int64(at), Millisecond), cb, udata)

proc callSoon*(acb: AsyncCallback) =
## Schedule `cbproc` to be called as soon as possible.
## The callback is called when control returns to the event loop.
getThreadDispatcher().callbacks.addLast(acb)
proc callSoon*(acb: AsyncCallback, immediate: static bool = false) =
## Schedule `acb` to be called as soon as possible.
## With `immediate`, no other scheduled callbacks and events are run.
## Otherwise, the callback is called when control returns to the event loop.
let loop = getThreadDispatcher()
when immediate:
loop.callbacks.addFirst(acb)
when chronosStrictReentrancy:
loop.numImmediate += 1
else:
loop.callbacks.addLast(acb)

proc callSoon*(cbproc: CallbackFunc, data: pointer) {.
gcsafe.} =
proc callSoon*(
cbproc: CallbackFunc, data: pointer,
immediate: static bool = false) {.gcsafe.} =
## Schedule `cbproc` to be called as soon as possible.
## The callback is called when control returns to the event loop.
## With `immediate`, no other scheduled callbacks and events are run.
## Otherwise, the callback is called when control returns to the event loop.
doAssert(not isNil(cbproc))
callSoon(AsyncCallback(function: cbproc, udata: data))
callSoon(AsyncCallback(function: cbproc, udata: data), immediate)

proc callSoon*(cbproc: CallbackFunc) =
callSoon(cbproc, nil)
proc callSoon*(cbproc: CallbackFunc, immediate: static bool = false) =
callSoon(cbproc, nil, immediate)

proc callIdle*(acb: AsyncCallback) =
## Schedule ``cbproc`` to be called when there no pending network events
Expand Down
74 changes: 61 additions & 13 deletions chronos/internal/asyncfutures.nim
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,13 @@ template newFuture*[T](fromProc: static[string] = "",
else:
newFutureImpl[T](getSrcLocation(fromProc), flags)

template newInternalRaisesFuture*[T, E](fromProc: static[string] = ""): auto =
template newInternalRaisesFuture*[T, E](
fromProc: static[string] = "", flags: static[FutureFlags] = {}): auto =
## Creates a new future.
##
## Specifying ``fromProc``, which is a string specifying the name of the proc
## that this future belongs to, is a good habit as it helps with debugging.
newInternalRaisesFutureImpl[T, E](getSrcLocation(fromProc), {})
newInternalRaisesFutureImpl[T, E](getSrcLocation(fromProc), flags)

template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] {.deprecated.} =
## Create a new future which can hold/preserve GC sequence until future will
Expand Down Expand Up @@ -182,6 +183,17 @@ proc finish(fut: FutureBase, state: FutureState, loc: ptr SrcLoc) =
fut.internalState = state
fut.internalCancelcb = nil # release cancellation callback memory

when chronosSyncContinuations:
proc process(callback: var AsyncCallback) =
if not(isNil(callback.function)):
callSoon(move(callback), immediate = true)

for i in countdown(fut.internalContinuations.high, 0):
process(fut.internalContinuations[i])
fut.internalContinuations = default(seq[AsyncCallback])

process(fut.internalContinuation)

if not(isNil(fut.internalCallback.function)):
callSoon(move(fut.internalCallback))

Expand Down Expand Up @@ -298,37 +310,71 @@ proc clearCallbacks(future: FutureBase) =
future.internalCallback.reset()
future.internalCallbacks.reset()

proc addCallback*(future: FutureBase, cb: CallbackFunc, udata: pointer) =
proc addCallback*(
future: FutureBase, cb: CallbackFunc, udata: pointer,
flags: static set[CallbackFlag] = {}) =
## Adds the callbacks proc to be called when the future completes.
##
## If future has already completed then ``cb`` will be called immediately.
doAssert(not isNil(cb))
if future.finished():
callSoon(cb, udata)

const _: set[CallbackFlag] = flags # https://github.com/nim-lang/Nim/issues/25938
let isContinuation =
when chronosSyncContinuations and CallbackFlag.Continuation in flags:
FutureFlag.SyncContinuations in future.internalFlags
else:
false

if isContinuation:
if future.finished():
callSoon(cb, udata, immediate = true)
else:
if isNil(future.internalContinuation.function):
future.internalContinuation = AsyncCallback(function: cb, udata: udata)
else:
future.internalContinuations.add(
AsyncCallback(function: cb, udata: udata))
else:
if isNil(future.internalCallback.function):
future.internalCallback = AsyncCallback(function: cb, udata: udata)
if future.finished():
callSoon(cb, udata)
else:
future.internalCallbacks.add AsyncCallback(function: cb, udata: udata)
if isNil(future.internalCallback.function):
future.internalCallback = AsyncCallback(function: cb, udata: udata)
else:
future.internalCallbacks.add(
AsyncCallback(function: cb, udata: udata))

proc addCallback*(future: FutureBase, cb: CallbackFunc) =
proc addCallback*(
future: FutureBase, cb: CallbackFunc,
flags: static set[CallbackFlag] = {}) =
## Adds the callbacks proc to be called when the future completes.
##
## If future has already completed then ``cb`` will be called immediately.
future.addCallback(cb, cast[pointer](future))
future.addCallback(cb, cast[pointer](future), flags)

proc removeCallback*(future: FutureBase, cb: CallbackFunc,
udata: pointer) =
## Remove future from list of callbacks - this operation may be slow if there
## are many registered callbacks!
doAssert(not isNil(cb))

template shouldRemove(callback: AsyncCallback): bool =
callback.function == cb and callback.udata == udata

# Make sure to release memory associated with callback, or reference chains
# may be created!
if future.internalCallback.function == cb and future.internalCallback.udata == udata:
when chronosSyncContinuations:
if shouldRemove(future.internalContinuation):
future.internalContinuation.reset()

future.internalContinuations.keepItIf:
not shouldRemove(it)

if shouldRemove(future.internalCallback):
future.internalCallback.reset()

future.internalCallbacks.keepItIf:
it.function != cb or it.udata != udata
not shouldRemove(it)

proc removeCallback*(future: FutureBase, cb: CallbackFunc) =
future.removeCallback(cb, cast[pointer](future))
Expand Down Expand Up @@ -394,7 +440,9 @@ proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.} =
# We cannot make progress on `fut` until `next` has finished - schedule
# `fut` to continue running when that happens
GC_ref(fut)
next.addCallback(CallbackFunc(internalContinue), cast[pointer](fut))
next.addCallback(
CallbackFunc(internalContinue), cast[pointer](fut),
{CallbackFlag.Continuation})

# return here so that we don't remove the closure below
return
Expand Down
5 changes: 3 additions & 2 deletions chronos/internal/asyncmacro.nim
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ proc asyncSingleProc(prc, params: NimNode): NimNode {.compileTime.} =
# with:
#
# ```nim
# let resultFuture = newFuture[T]()
# let resultFuture = newFuture[T]({FutureFlag.SyncContinuations})
# resultFuture.internalClosure = `iteratorNameSym`
# futureContinue(resultFuture)
# return resultFuture
Expand Down Expand Up @@ -545,7 +545,8 @@ proc asyncSingleProc(prc, params: NimNode): NimNode {.compileTime.} =
outerProcBody.add(
newLetStmt(
retFutureSym,
newCall(newFutProc, newLit(prcName))
newCall(newFutProc, newLit(prcName), nnkCurly.newTree(
newDotExpr(ident "FutureFlag", ident "SyncContinuations")))
)
)

Expand Down
8 changes: 4 additions & 4 deletions tests/testall.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import ../chronos/config

import
./[
testmacro, testsync, testsoon, testtime, testfut, testaddress,
testdatagram, teststream, testserver, testbugs, testnet, testasyncstream,
testhttpserver, testshttpserver, testhttpclient, testratelimit,
testfutures, testthreadsync, testasyncsemaphore,
testmacro, testsync, testsoon, testcontinuations, testtime, testfut,
testaddress, testdatagram, teststream, testserver, testbugs, testnet,
testasyncstream, testhttpserver, testshttpserver, testhttpclient,
testratelimit, testfutures, testthreadsync, testasyncsemaphore,
]

when (chronosEventEngine in ["epoll", "kqueue"]) or defined(windows):
Expand Down
Loading