Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EventLoop APIs for cheaper setting of timers #2759

Open
wants to merge 32 commits into
base: main
Choose a base branch
from

Conversation

simonjbeaumont
Copy link
Contributor

Motivation

Scheduling a timer is currently an expensive operation because this is done using EventLoop.scheduleTask(in:_:), which results in several allocations. A simple timer use case does not typically need all the functionality of this API and use cases that set many timers (e.g. for timeouts) pay this cost repeatedly.

Modifications

This PR introduces new protocol requirements on EventLoop:

protocol EventLoop {
    // ...
    func setTimer(for deadline: NIODeadline, _ handler: any NIOTimerHandler) -> NIOTimer
    func setTimer(for duration: TimeAmount, _ handler: any NIOTimerHandler) -> NIOTimer
}

Default implementations have been provided that call through to EventLoop.scheduleTask(in:_:) to not break existing EventLoop implementations, although this implementation will be (at least) as slow as using scheduleTask(in:_:) directly.

This PR also introduces an opt-in protocol refinement, which allows for EventLoop implementations to provide a custom timer backing, as an optimization point:

public protocol NIOCustomTimerImplementation: EventLoop {
    func setTimer(for deadline: NIODeadline, _ handler: any NIOSimpleTimerHandler) -> UInt64
    func cancelTimer(_ id: UInt64)
}

Following that, this PR adds an extension to SelectableEventLoop with NIOCustomTimerImplementation conformance, so that MultiThreadedEventLoopGroup can benefit from a faster timer implementation.

Finally, this PR adds benchmarks to measure the performance of setting a simple timer using both scheduleTask(in:_:) and setTimer(for:_:) APIs using a MultiThreadedEventLoopGroup.

The changes are scoped to their own commits for easier review:

  • benchmarks: Add benchmark for MTELG.scheduleTask(in:_:).
  • api: Add NIOTimer, NIOTimerHandler, NIOCustomTimerImplementation, and EventLoop.setTimer(for:_:).
  • benchmarks: Add benchmark for MTELG.setTimer(for:_:).
  • internal: Add NIOCustomTimerImplementation conformance to SelectableEventLoop.

Result

When using MTELG to repeatedly set a timer with the same handler, switching from scheduleTask(in:_:) to setTimer(for:_:) reduces almost all allocations (and amortizes to zero allocations) and is twice as fast.

MTELG.scheduleTask(in:_:)
╒═════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                  │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞═════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) *        │       4 │       4 │       4 │       4 │       4 │       4 │       4 │     520 │
├─────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Time (total CPU) (ns) * │     447 │     719 │     773 │     834 │     908 │    1098 │    1348 │     520 │
╘═════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛
MTELG.setTimer(for:_:) without NIOCustomTimerImplementation conformance
╒═════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                  │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞═════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) *        │       5 │       5 │       5 │       5 │       5 │       5 │       5 │     476 │
├─────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Time (total CPU) (ns) * │     482 │     760 │     823 │     905 │     966 │    1119 │    1364 │     476 │
╘═════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛
MTELG.setTimer(for:_:) with NIOCustomTimerImplementation conformance
╒═════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                  │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞═════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) *        │       0 │       0 │       0 │       0 │       0 │       0 │       0 │    1071 │
├─────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Time (total CPU) (ns) * │     178 │     278 │     327 │     383 │     434 │     535 │     664 │    1071 │
╘═════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

@simonjbeaumont
Copy link
Contributor Author

@swift-server-bot test this please

Sources/NIOCore/EventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOCore/NIOTimer.swift Outdated Show resolved Hide resolved
Sources/NIOCore/EventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOCore/EventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOCore/EventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOCore/NIOTimer.swift Outdated Show resolved Hide resolved
Sources/NIOCore/NIOTimer.swift Outdated Show resolved Hide resolved
Sources/NIOCore/EventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOCore/EventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOCore/NIOTimer.swift Outdated Show resolved Hide resolved
Sources/NIOCore/EventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOCore/EventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOCore/NIOTimer.swift Outdated Show resolved Hide resolved
/// A task created using `EventLoop.scheduleTask(deadline:_:)`, used by default for `EventLoop` implementations.
case scheduledTask(Scheduled<Void>)
/// An identifier for a timer, used by `EventLoop` implementations that conform to `CustomTimerImplementation`.
case custom(eventLoop: any NIOCustomTimerImplementation, id: UInt64)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of storing the event loop here I am wondering if we can just store a cancel closure. I think I tried that with ScheduledTask once but I am unsure if I got it down to 0 allocations. That might allow us to avoid the whole UInt64 completely

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try this again, but my intuition is that this will not be as performant. Is avoiding the UInt64 a goal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this and it resulted in an allocation. I have kept the commits in the history so you can see the attempt and the revert:

  • b3e4903: feedback(attempted): Store a closure instead of UInt64
  • fb5e835: feedback(reverted, allocates): Store a closure instead of UInt64

extension SelectableEventLoop: NIOCustomTimerImplementation {
@inlinable
internal func setTimer(for deadline: NIODeadline, _ handler: any NIOTimerHandler) -> UInt64 {
let taskId = self.scheduledTaskCounter.loadThenWrappingIncrement(ordering: .relaxed)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now have two places here that create a task id and then a ScheduledTask can we de-dupe the code into a shared method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep the CustomTimerImplementation conformance in its own extension here for clarity but, yes, let me take a look and see what refactoring I can do in the SelectableEventLoop to provide a reusable method.

Comment on lines 909 to 911
self._tasksLock.withLock {
self._scheduledTasks.removeFirst(where: { $0.id == id })
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar here can we de-dupe this code

Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift Outdated Show resolved Hide resolved
}

/// Extension point for `EventLoop` implementations implement a custom timer.
public protocol NIOCustomTimerImplementation: EventLoop {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary for us to add a new protocol? Any reason not to have EventLoop provide this directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I tried to outline this in the PR description, but the goal here is to have a simple default implementation and have this as an opt-in extension point for implementations that want to do something different (e.g. more performant).

If we elevated these to the EventLoop protocol how would the default implementation look? Additionally, the protocol refinement here allows the type system to enforce that an implementation that's opting in here implements both halves of the customisation point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we ever want the default implementation: it's almost always worse than doing something bespoke. We have to provide it to avoid the API break, but we don't really care about it.

If we elevated these to the EventLoop protocol how would the default implementation look?

More or less exactly as it does now. cancel is straightforward to implement, as you already have.

Additionally, the protocol refinement here allows the type system to enforce that an implementation that's opting in here implements both halves of the customisation point.

This is handy, but doesn't necessarily outweigh the noise of the extra protocol.

More broadly, because we have added a new protocol, we make all event loops publicly offer these two methods:

    func setTimer(for deadline: NIODeadline, _ handler: any NIOTimerHandler) -> UInt64
    func cancelTimer(_ id: UInt64)

But we don't want these two methods, we want the two that return typed handles. Nonetheless, users will see and be able to call these two on event loops. I think that's a strong enough API argument to reject this spelling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More broadly, because we have added a new protocol, we make all event loops publicly offer these two methods:

    func setTimer(for deadline: NIODeadline, _ handler: any NIOTimerHandler) -> UInt64
    func cancelTimer(_ id: UInt64)

But we don't want these two methods, we want the two that return typed handles. Nonetheless, users will see and be able to call these two on event loops. I think that's a strong enough API argument to reject this spelling.

Presumably it would be only event loops that conform to this protocol that would publicly expose these methods and only when they are being held as a concrete type, rather than an any EventLoop?

However, I do concede that these symbols end up being public when ideally they should be internal.

I did this because I thought that was what the design called for: a slow default and the ability to provide something faster.

Maybe I've been thinking about exposing the wrong half of this. In the current PR I'm exposing the new methods (as a new protocol with new methods) and keeping the typed handle very opaque. Are you suggesting that I make the typed NIOTimer less opaque. Specifically that users can construct it as either a scheduledTask or a custom (will think on a better name if it's going to be public) which would allow event loop implementations to still opt in to provide a more performant implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so I've gone ahead and made the patch that I believe is more in line with this feedback.

We no longer have another protocol, but instead we now have to be careful about ensuring we provide the right overloads for both the schedule and cancel functions and need a runtime crash if the contract has been violated.

IMO, this was a place we could have had the type system help us and any external authors of event loops.

I appreciate the argument that it would result in the exposing of lower level functionality to end users, but they don't have to call that, and, if they did it would be safe still.

Possibly a moot point now, but would you have considered a layered approach like that initially proposed if we made use of @_spi to expose it only to event loop implementors?

The targeted relevant changes for this feedback are in 89c57ec.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly a moot point now, but would you have considered a layered approach like that initially proposed if we made use of @_spi to expose it only to event loop implementors?

I'm not convinced this would actually have worked. It's not immediately clear to me what the semantics of conformance to @_spi protocols actually is, so it's not clear to me that this would have prevented the protocol conformance being essentially public or that it would have successfully populated the witness table.

If we assume that it would work as intended, though, I'm still not sure I'd have gone for it. I feel less strongly: the extra API noise is removed. But it would be a bit challenging to communicate to implementors how they should provide this implementation. Ultimately, we'd have to document it: otherwise it won't occur to implementors that they should implement this other protocol to implement these two functions. We have a lot of protocols, and it'll be easy to miss.

Once we're relying on documentation for correct implementation, I don't know that it's much worse to simply document that implementors must provide both methods. Yes, the type system will help correct implementations, but incorrect implementations will fail almost immediately the moment anyone tries to use them, including the implementor. So I'm just fundamentally not convinced that the downside risk is worth the extra abstractions.


/// Set a timer that will call a handler at the given time.
@discardableResult
func setTimer(for deadline: NIODeadline, _ handler: any NIOTimerHandler) -> NIOTimer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add another bikeshed: is there any reason not to make this method generic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 06a4ce7. Although I didn't see much improvement from it.

Sources/NIOCore/NIOTimer.swift Outdated Show resolved Hide resolved

/// Set a timer that will call a handler after a given amount of time.
@discardableResult
func setTimer(for duration: TimeAmount, _ handler: any NIOTimerHandler) -> NIOTimer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I'm here, can I suggest that we should use different labels instead of for in both? That will help with tab-completion a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it felt OK to me. Would you prefer the following?

  • setTimer(for: TimeAmount, ...)
  • setTimer(forDeadline: NIODeadline, ...)

I'm thinking of how I set timers verbally on my phone I normally always use "set a timer for" regardless of whether I then say a duration or an absolute time.

Open to suggestions here, though :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setTimer(for:) and setTimer(at:) is probably the easiest spelling distinction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/apple/swift-nio/pull/2759/files/df3cdb1c1f1f3ce8fb66fd81cdfa0cef3318c984#r1664313726 resulted in new API spellings. But these do use different prepositions for the NIODeadline and TimeAmount variants: scheduleCallback(at:handler:) and scheduleCallback(in:handler:), respectively.

Sources/NIOCore/NIOTimer.swift Outdated Show resolved Hide resolved
Copy link
Contributor Author

@simonjbeaumont simonjbeaumont left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback so far folks. I've addressed it all and attempted to do so in a series of targeted commits that can be squashed on merge.

//cc @Lukasa @glbrntt @FranzBusch


/// Set a timer that will call a handler after a given amount of time.
@discardableResult
func setTimer(for duration: TimeAmount, _ handler: any NIOTimerHandler) -> NIOTimer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/apple/swift-nio/pull/2759/files/df3cdb1c1f1f3ce8fb66fd81cdfa0cef3318c984#r1664313726 resulted in new API spellings. But these do use different prepositions for the NIODeadline and TimeAmount variants: scheduleCallback(at:handler:) and scheduleCallback(in:handler:), respectively.

/// A task created using `EventLoop.scheduleTask(deadline:_:)`, used by default for `EventLoop` implementations.
case scheduledTask(Scheduled<Void>)
/// An identifier for a timer, used by `EventLoop` implementations that conform to `CustomTimerImplementation`.
case custom(eventLoop: any NIOCustomTimerImplementation, id: UInt64)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this and it resulted in an allocation. I have kept the commits in the history so you can see the attempt and the revert:

  • b3e4903: feedback(attempted): Store a closure instead of UInt64
  • fb5e835: feedback(reverted, allocates): Store a closure instead of UInt64


/// Set a timer that will call a handler at the given time.
@discardableResult
func setTimer(for deadline: NIODeadline, _ handler: any NIOTimerHandler) -> NIOTimer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 06a4ce7. Although I didn't see much improvement from it.

}

/// Extension point for `EventLoop` implementations implement a custom timer.
public protocol NIOCustomTimerImplementation: EventLoop {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so I've gone ahead and made the patch that I believe is more in line with this feedback.

We no longer have another protocol, but instead we now have to be careful about ensuring we provide the right overloads for both the schedule and cancel functions and need a runtime crash if the contract has been violated.

IMO, this was a place we could have had the type system help us and any external authors of event loops.

I appreciate the argument that it would result in the exposing of lower level functionality to end users, but they don't have to call that, and, if they did it would be safe still.

Possibly a moot point now, but would you have considered a layered approach like that initially proposed if we made use of @_spi to expose it only to event loop implementors?

The targeted relevant changes for this feedback are in 89c57ec.

Copy link
Contributor

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the method name for NIOScheduledCallbackHandler but beyond that this looks good aside from some nits.

/// Schedule a callback at a given time.
///
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** implement _both_
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
/// `scheduleCallback(at:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're converging on the naming I've made these proper DocC references instead in 5d6ac17.

/// Schedule a callback after given time.
///
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** implement _both_
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
/// `scheduleCallback(at:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're converging on the naming I've made these proper DocC references instead in 5d6ac17.

/// Cancel a scheduled callback.
///
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** implement _both_
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
/// `scheduleCallback(at:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment applies to a few other places too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're converging on the naming I've made these proper DocC references instead in 5d6ac17.

/// This function is called at the scheduled time, unless the scheduled callback is cancelled.
///
/// - Parameter eventLoop: The event loop on which the callback was scheduled.
func onSchedule(eventLoop: some EventLoop)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming reads (to me) like this function is called at the time when the callback is scheduled (when eventLoop.scheduledCallback is called), as opposed to the time when the callback is scheduled to run.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleScheduledCallback?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the name to handleScheduledCallback in 2ec5543.

Question: do we need to be more considerate about name clashes since we're expecting folks to conform their types to this protocol; e.g. should this be something like handleNIOScheduledCallback?

This case isn't quite covered by https://github.com/apple/swift-nio/blob/main/docs/public-api.md, but it seems similar in nature.

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glbrntt did you have any thoughts regarding the namespacing or is it fine the way it is?

The function takes a NIO type, so at worst it could cause an overload? Maybe it's OK the way it is now.

Sources/NIOPosix/SelectableEventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/SelectableEventLoop.swift Outdated Show resolved Hide resolved
Copy link
Contributor Author

@simonjbeaumont simonjbeaumont left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @glbrntt for your latest round of review. I have addressed most of the feedback. I wanted some additional thoughts on these two though:

Sources/NIOPosix/SelectableEventLoop.swift Outdated Show resolved Hide resolved
/// Schedule a callback after given time.
///
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** implement _both_
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're converging on the naming I've made these proper DocC references instead in 5d6ac17.

/// Schedule a callback at a given time.
///
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** implement _both_
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're converging on the naming I've made these proper DocC references instead in 5d6ac17.

/// Cancel a scheduled callback.
///
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** implement _both_
/// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're converging on the naming I've made these proper DocC references instead in 5d6ac17.

Sources/NIOPosix/SelectableEventLoop.swift Outdated Show resolved Hide resolved
/// This function is called at the scheduled time, unless the scheduled callback is cancelled.
///
/// - Parameter eventLoop: The event loop on which the callback was scheduled.
func onSchedule(eventLoop: some EventLoop)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the name to handleScheduledCallback in 2ec5543.

Question: do we need to be more considerate about name clashes since we're expecting folks to conform their types to this protocol; e.g. should this be something like handleNIOScheduledCallback?

This case isn't quite covered by https://github.com/apple/swift-nio/blob/main/docs/public-api.md, but it seems similar in nature.

WDYT?

Copy link
Contributor

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try! needs fixing but otherwise I'm happy with this!

Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift Outdated Show resolved Hide resolved
Sources/NIOCore/NIOScheduledCallback.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/SelectableEventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/MultiThreadedEventLoopGroup.swift Outdated Show resolved Hide resolved
@simonjbeaumont simonjbeaumont marked this pull request as ready for review July 11, 2024 14:27
Sources/NIOEmbedded/AsyncTestingEventLoop.swift Outdated Show resolved Hide resolved
/// This function is called at the scheduled time, unless the scheduled callback is cancelled.
///
/// - Parameter eventLoop: The event loop on which the callback was scheduled.
func handleScheduledCallback(eventLoop: some EventLoop)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be an optional method to implement when the scheduled callback is cancelled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure really. I think we'd need to see how people use this a bit more to form an opinion on whether it's useful.

Also, if we were to provide it, would it be cleaner to add a non-optional requirement with a default empty implementation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-optional requirement default implementation is Swift's equivalent of optional requirement.

FWIW I think having an onCancel callback is useful. It allows you to centralise the state keeping into a single place.

Comment on lines 76 to 77
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { try! group.syncShutdownGracefully() }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do this setup inside the benchmark.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? IIUC, that's what startMeasurement is for.

If you're suggesting that I use the setup and teardown closures of the Benchmark() function, then I cannot since I need to actually use these variables in the benchmark. The only alternative would be to make them implicitly unwrapped optionals at global scope, which was pretty gross.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can reuse ELs across benchmarks so I was thinking it might just be better if we create one EL that we share. I would just define a let eventLoop = MultithreadedEventLoopGroup.singleton.any() at the top.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that's great, I think I'd rather each benchmark do it's own setup and teardown to ensure its unpolluted by the side effects of running other benchmarks.

Where we can, I'm happy to try and use the setup/teardown closure style, if you prefer it, and where we cannot, use this.

It also has the benefit for local reasoning of what's being benchmarked. It's all contained in the call to Benchmark { ... }.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree and I just looked at the code in the benchmark again and we are already defining an EL for exactly this purpose. There is a static EL defined in this file called eventLoop.

The reason why I think it's not important to keep is that it should be irrelevant for the benchmark here. The important part of the benchmark is the scheduling and not how the EL is constructed and shut down so it keeps the benchmark more concise.

IMO we should definitely only have one style here and not mix static EL with one EL per benchmark.

Benchmarks/Benchmarks/NIOPosixBenchmarks/Benchmarks.swift Outdated Show resolved Hide resolved
Sources/NIOCore/NIOScheduledCallback.swift Outdated Show resolved Hide resolved
Sources/NIOEmbedded/Embedded.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/MultiThreadedEventLoopGroup.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/MultiThreadedEventLoopGroup.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/SelectableEventLoop.swift Outdated Show resolved Hide resolved
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** also implement
/// `cancelScheduledCallback`. Failure to do so will result in a runtime error.
@discardableResult
func scheduleCallback(at deadline: NIODeadline, handler: some NIOScheduledCallbackHandler) throws -> NIOScheduledCallback
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: I personally find callback an overloaded term here since it is mostly used for meaning closure in Swift/ObjC. I liked the previous timer naming.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this context, isn't it actually a callback being scheduled though?

Copy link
Contributor Author

@simonjbeaumont simonjbeaumont Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked the previous timer naming.

😭 😰

IMO @glbrntt is right here. This is a callback in the truest sense of what is being scheduled.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. It was just a nit and I am fine with both

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to prefer the timer terminology but was convinced by @glbrntt's arguments and it did make the API names much more self-documenting.

/// This function is called at the scheduled time, unless the scheduled callback is cancelled.
///
/// - Parameter eventLoop: The event loop on which the callback was scheduled.
func handleScheduledCallback(eventLoop: some EventLoop)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-optional requirement default implementation is Swift's equivalent of optional requirement.

FWIW I think having an onCancel callback is useful. It allows you to centralise the state keeping into a single place.

Copy link
Contributor

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM modulo a couple of nits.

/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** also implement
/// `cancelScheduledCallback`. Failure to do so will result in a runtime error.
@discardableResult
func scheduleCallback(at deadline: NIODeadline, handler: some NIOScheduledCallbackHandler) throws -> NIOScheduledCallback
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this context, isn't it actually a callback being scheduled though?

Sources/NIOEmbedded/AsyncTestingEventLoop.swift Outdated Show resolved Hide resolved
Sources/NIOEmbedded/Embedded.swift Outdated Show resolved Hide resolved
@simonjbeaumont
Copy link
Contributor Author

In order to support cancellation, I'm gonna wait to land #2800.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants