Skip to content

Commit 96b0d8b

Browse files
committed
Extend NIOEventLoopTickInfo with end-time and loop sleep time
Motivation: NIO’s event loop can emit metrics. Currently, this includes the start time (i.e. when the loop started working) and the number of tasks which were executed in that tick. It would be useful to collect the time the loop tick ended and the length of time the loop was asleep before starting the tick. Modifications: The event loop has a new property to track the time between runs. The tick info collects two new values: * The time since the last run. * The time the loop finished. Result: The new metrics are available for collection.
1 parent c9e2ac1 commit 96b0d8b

File tree

2 files changed

+42
-8
lines changed

2 files changed

+42
-8
lines changed

Sources/NIOPosix/SelectableEventLoop.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,25 @@ public struct NIOEventLoopTickInfo: Sendable, Hashable {
5353
public var eventLoopID: ObjectIdentifier
5454
/// The number of tasks which were executed in this tick
5555
public var numberOfTasks: Int
56+
/// The time the event loop slept since the last tick
57+
public var sleepTime: TimeAmount
5658
/// The time at which the tick began
5759
public var startTime: NIODeadline
60+
/// The time at which the tick finished
61+
public var endTime: NIODeadline
5862

59-
internal init(eventLoopID: ObjectIdentifier, numberOfTasks: Int, startTime: NIODeadline) {
63+
internal init(
64+
eventLoopID: ObjectIdentifier,
65+
numberOfTasks: Int,
66+
sleepTime: TimeAmount,
67+
startTime: NIODeadline,
68+
endTime: NIODeadline
69+
) {
6070
self.eventLoopID = eventLoopID
6171
self.numberOfTasks = numberOfTasks
72+
self.sleepTime = sleepTime
6273
self.startTime = startTime
74+
self.endTime = endTime
6375
}
6476
}
6577

@@ -169,6 +181,8 @@ internal final class SelectableEventLoop: EventLoop, @unchecked Sendable {
169181

170182
private let metricsDelegate: (any NIOEventLoopMetricsDelegate)?
171183

184+
private var lastTickEndTime: NIODeadline
185+
172186
@usableFromInline
173187
internal func _promiseCreated(futureIdentifier: _NIOEventLoopFutureIdentifier, file: StaticString, line: UInt) {
174188
precondition(_isDebugAssertConfiguration())
@@ -246,6 +260,7 @@ internal final class SelectableEventLoop: EventLoop, @unchecked Sendable {
246260
self.msgBufferPool = Pool<PooledMsgBuffer>(maxSize: 16)
247261
self.tasksCopy.reserveCapacity(Self.tasksCopyBatchSize)
248262
self.canBeShutdownIndividually = canBeShutdownIndividually
263+
self.lastTickEndTime = .now()
249264
// note: We are creating a reference cycle here that we'll break when shutting the SelectableEventLoop down.
250265
// note: We have to create the promise and complete it because otherwise we'll hit a loop in `makeSucceededFuture`. This is
251266
// fairly dumb, but it's the only option we have.
@@ -722,14 +737,19 @@ internal final class SelectableEventLoop: EventLoop, @unchecked Sendable {
722737

723738
private func runLoop(selfIdentifier: ObjectIdentifier) -> NIODeadline? {
724739
let tickStartTime: NIODeadline = .now()
740+
let sleepTime: TimeAmount = tickStartTime - self.lastTickEndTime
725741
var tasksProcessedInTick = 0
726742
defer {
743+
let tickEndTime: NIODeadline = .now()
727744
let tickInfo = NIOEventLoopTickInfo(
728745
eventLoopID: selfIdentifier,
729746
numberOfTasks: tasksProcessedInTick,
730-
startTime: tickStartTime
747+
sleepTime: sleepTime,
748+
startTime: tickStartTime,
749+
endTime: tickEndTime
731750
)
732751
self.metricsDelegate?.processedTick(info: tickInfo)
752+
self.lastTickEndTime = tickEndTime
733753
}
734754
while true {
735755
let nextReadyDeadline = self._tasksLock.withLock { () -> NIODeadline? in

Tests/NIOPosixTests/EventLoopMetricsDelegateTests.swift

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,36 @@ final class EventLoopMetricsDelegateTests: XCTestCase {
4949
XCTAssertEqual(delegate.infos.count, 0)
5050

5151
let promise = el.makePromise(of: Void.self)
52+
el.scheduleTask(in: .milliseconds(100)) {
53+
// Nop. Ensures that we collect multiple tick infos.
54+
}
5255
el.scheduleTask(in: .seconds(1)) {
5356
promise.succeed()
5457
}
58+
5559
promise.futureResult.whenSuccess {
56-
// There are 3 tasks (scheduleTask, whenSuccess, wait) which can trigger a total of 1...3 ticks
57-
XCTAssertTrue((1...3).contains(delegate.infos.count), "Expected 1...3 ticks, got \(delegate.infos.count)")
58-
// the total number of tasks across these ticks should be either 2 or 3
60+
// There are 4 tasks (scheduleTask, scheduleTask, whenSuccess, wait) which can trigger a total of 2...4 ticks
61+
XCTAssertTrue((2...4).contains(delegate.infos.count), "Expected 2...4 ticks, got \(delegate.infos.count)")
62+
// The total number of tasks across these ticks should be either 2 or 3
5963
let totalTasks = delegate.infos.map { $0.numberOfTasks }.reduce(0, { $0 + $1 })
60-
XCTAssertTrue((2...3).contains(totalTasks), "Expected 2...3 tasks, got \(totalTasks)")
64+
XCTAssertTrue((3...4).contains(totalTasks), "Expected 3...4 tasks, got \(totalTasks)")
65+
// All tasks were run by the same event loop. The measurements are monotonically increasing.
66+
var lastEndTime: NIODeadline?
6167
for info in delegate.infos {
6268
XCTAssertEqual(info.eventLoopID, ObjectIdentifier(el))
69+
XCTAssertTrue(info.startTime < info.endTime)
70+
// If this is not the first tick, verify the sleep time.
71+
if let lastEndTime {
72+
XCTAssertTrue(lastEndTime < info.startTime)
73+
XCTAssertEqual(lastEndTime + info.sleepTime, info.startTime)
74+
}
75+
// Keep track of the last event time to verify the sleep interval.
76+
lastEndTime = info.endTime
6377
}
6478
if let lastTickStartTime = delegate.infos.last?.startTime {
6579
let timeSinceStart = lastTickStartTime - testStartTime
66-
// This should be near instant, limiting to 100ms
67-
XCTAssertLessThan(timeSinceStart.nanoseconds, 100_000_000)
80+
// This should be near instantly after the delay of the first run.
81+
XCTAssertLessThan(timeSinceStart.nanoseconds, 200_000_000)
6882
XCTAssertGreaterThan(timeSinceStart.nanoseconds, 0)
6983
}
7084
}

0 commit comments

Comments
 (0)