Skip to content

Commit

Permalink
Don't re-arm timerfd each epoll_wait (#264)
Browse files Browse the repository at this point in the history
Motivation:

When calling Selector.whenReady(...) and using a SelectorStrategy.blockUntil(...) we may end up re-arm the timerfd each time even if the wakeup time did not change.
This is expensive and can be avoided in most situations.

Modifications:

Keep track of the earliest scheduled timer and only schedule a new one if it would fire sooner.

Result:

Less overhead when using epoll and timers.
  • Loading branch information
normanmaurer authored and weissi committed Apr 3, 2018
1 parent ba470b7 commit 2a684fc
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 3 deletions.
20 changes: 17 additions & 3 deletions Sources/NIO/Selector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

import Dispatch

private enum SelectorLifecycleState {
case open
case closing
Expand Down Expand Up @@ -50,6 +52,7 @@ final class Selector<R: Registration> {
private typealias EventType = Epoll.epoll_event
private let eventfd: Int32
private let timerfd: Int32
private var earliestTimer: UInt64 = UInt64.max
#else
private typealias EventType = kevent
#endif
Expand Down Expand Up @@ -352,9 +355,17 @@ final class Selector<R: Registration> {
case .now:
ready = Int(try Epoll.epoll_wait(epfd: self.fd, events: events, maxevents: Int32(eventsCapacity), timeout: 0))
case .blockUntilTimeout(let timeAmount):
var ts = itimerspec()
ts.it_value = timespec(timeAmount: timeAmount)
try TimerFd.timerfd_settime(fd: timerfd, flags: 0, newValue: &ts, oldValue: nil)
// Only call timerfd_settime if we not already scheduled one that will cover it.
// This guards against calling timerfd_settime if not needed as this is generally speaking
// expensive.
let next = DispatchTime.now().uptimeNanoseconds + UInt64(timeAmount.nanoseconds)
if next < self.earliestTimer {
self.earliestTimer = next

var ts = itimerspec()
ts.it_value = timespec(timeAmount: timeAmount)
try TimerFd.timerfd_settime(fd: timerfd, flags: 0, newValue: &ts, oldValue: nil)
}
fallthrough
case .block:
ready = Int(try Epoll.epoll_wait(epfd: self.fd, events: events, maxevents: Int32(eventsCapacity), timeout: -1))
Expand All @@ -372,6 +383,9 @@ final class Selector<R: Registration> {
var val: UInt = 0
// We are not interested in the result
_ = Glibc.read(timerfd, &val, MemoryLayout<UInt>.size)

// Processed the earliest set timer so reset it.
self.earliestTimer = UInt64.max
default:
// If the registration is not in the Map anymore we deregistered it during the processing of whenReady(...). In this case just skip it.
if let registration = registrations[Int(ev.data.fd)] {
Expand Down
1 change: 1 addition & 0 deletions Tests/NIOTests/EventLoopTest+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ extension EventLoopTest {
("testCurrentEventLoop", testCurrentEventLoop),
("testShutdownWhileScheduledTasksNotReady", testShutdownWhileScheduledTasksNotReady),
("testCloseFutureNotifiedBeforeUnblock", testCloseFutureNotifiedBeforeUnblock),
("testScheduleMultipleTasks", testScheduleMultipleTasks),
]
}
}
Expand Down
42 changes: 42 additions & 0 deletions Tests/NIOTests/EventLoopTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,46 @@ public class EventLoopTest : XCTestCase {
XCTAssertTrue(channel.closeFuture.isFulfilled)
XCTAssertFalse(channel.isActive)
}

public func testScheduleMultipleTasks() throws {
let nanos = DispatchTime.now().uptimeNanoseconds
let amount: TimeAmount = .seconds(1)
let eventLoopGroup = MultiThreadedEventLoopGroup(numThreads: 1)
defer {
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
}
var array = Array<(Int, DispatchTime)>()
let scheduled1 = eventLoopGroup.next().scheduleTask(in: .milliseconds(500)) {
array.append((1, DispatchTime.now()))
}

let scheduled2 = eventLoopGroup.next().scheduleTask(in: .milliseconds(100)) {
array.append((2, DispatchTime.now()))
}

let scheduled3 = eventLoopGroup.next().scheduleTask(in: .milliseconds(1000)) {
array.append((3, DispatchTime.now()))
}

var result = try eventLoopGroup.next().scheduleTask(in: .milliseconds(1000)) {
array
}.futureResult.wait()

XCTAssertTrue(scheduled1.futureResult.isFulfilled)
XCTAssertTrue(scheduled2.futureResult.isFulfilled)
XCTAssertTrue(scheduled3.futureResult.isFulfilled)

let first = result.removeFirst()
XCTAssertEqual(2, first.0)
let second = result.removeFirst()
XCTAssertEqual(1, second.0)
let third = result.removeFirst()
XCTAssertEqual(3, third.0)

XCTAssertTrue(first.1 < second.1)
XCTAssertTrue(second.1 < third.1)

XCTAssertTrue(result.isEmpty)

}
}

0 comments on commit 2a684fc

Please # to comment.