Skip to content

Commit 9b86559

Browse files
authored
Pre-box some errors to reduce allocations (#2765)
Motivation: Existential errors (`any Error`) are unconditionally boxed. In NIO we often create errors to fail promises. One place we do this is in the head channel handler when the pipeline is closed. This error is created regardless of whether there are actually any promises to fail. The result is an unnecessary allocation when every channel is closed. This is particuarly egregious when multiplexed protocols such as HTTP/2 are used. As the majority of errors in NIO carry no information beyond their type/name we can declare these statically and pay the cost of each error just once rather than once per use. Modifications: - Add internal static constants for `ChannelError` and `EventLoopError` and use them throughout NIOCore and NIOPosix where they would otherwise be boxed. Result: Fewer allocations
1 parent 9614996 commit 9b86559

21 files changed

+210
-156
lines changed

Sources/NIOCore/AsyncChannel/AsyncChannelOutboundWriterHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ internal final class NIOAsyncChannelOutboundWriterHandler<OutboundOut: Sendable>
137137
@inlinable
138138
func handlerRemoved(context: ChannelHandlerContext) {
139139
self.context = nil
140-
self.sink?.finish(error: ChannelError.ioOnClosedChannel)
140+
self.sink?.finish(error: ChannelError._ioOnClosedChannel)
141141
self.writer = nil
142142
}
143143

@@ -150,7 +150,7 @@ internal final class NIOAsyncChannelOutboundWriterHandler<OutboundOut: Sendable>
150150

151151
@inlinable
152152
func channelInactive(context: ChannelHandlerContext) {
153-
self.sink?.finish(error: ChannelError.ioOnClosedChannel)
153+
self.sink?.finish(error: ChannelError._ioOnClosedChannel)
154154
context.fireChannelInactive()
155155
}
156156

Sources/NIOCore/Channel.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ extension Channel {
202202
}
203203

204204
public func registerAlreadyConfigured0(promise: EventLoopPromise<Void>?) {
205-
promise?.fail(ChannelError.operationUnsupported)
205+
promise?.fail(ChannelError._operationUnsupported)
206206
}
207207

208208
public func triggerUserOutboundEvent(_ event: Any, promise: EventLoopPromise<Void>?) {
@@ -373,6 +373,17 @@ public enum ChannelError: Error {
373373
case unremovableHandler
374374
}
375375

376+
extension ChannelError {
377+
// 'any Error' is unconditionally boxed, avoid allocating per use by statically boxing them.
378+
static let _alreadyClosed: any Error = ChannelError.alreadyClosed
379+
static let _inputClosed: any Error = ChannelError.inputClosed
380+
@usableFromInline
381+
static let _ioOnClosedChannel: any Error = ChannelError.ioOnClosedChannel
382+
static let _operationUnsupported: any Error = ChannelError.operationUnsupported
383+
static let _outputClosed: any Error = ChannelError.outputClosed
384+
static let _unremovableHandler: any Error = ChannelError.unremovableHandler
385+
}
386+
376387
extension ChannelError: Equatable { }
377388

378389
/// The removal of a `ChannelHandler` using `ChannelPipeline.removeHandler` has been attempted more than once.

Sources/NIOCore/ChannelPipeline.swift

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public final class ChannelPipeline: ChannelInvoker {
198198
self.eventLoop.assertInEventLoop()
199199

200200
if self.destroyed {
201-
return .failure(ChannelError.ioOnClosedChannel)
201+
return .failure(ChannelError._ioOnClosedChannel)
202202
}
203203

204204
switch position {
@@ -247,7 +247,7 @@ public final class ChannelPipeline: ChannelInvoker {
247247
operation: (ChannelHandlerContext, ChannelHandlerContext) -> Void) -> Result<Void, Error> {
248248
self.eventLoop.assertInEventLoop()
249249
if self.destroyed {
250-
return .failure(ChannelError.ioOnClosedChannel)
250+
return .failure(ChannelError._ioOnClosedChannel)
251251
}
252252

253253
guard let context = self.contextForPredicate0({ $0.handler === relativeHandler }) else {
@@ -280,7 +280,7 @@ public final class ChannelPipeline: ChannelInvoker {
280280
self.eventLoop.assertInEventLoop()
281281

282282
if self.destroyed {
283-
return .failure(ChannelError.ioOnClosedChannel)
283+
return .failure(ChannelError._ioOnClosedChannel)
284284
}
285285

286286
let context = ChannelHandlerContext(name: name ?? nextName(), handler: handler, pipeline: self)
@@ -416,7 +416,7 @@ public final class ChannelPipeline: ChannelInvoker {
416416
/// - promise: An `EventLoopPromise` that will complete when the `ChannelHandler` is removed.
417417
public func removeHandler(context: ChannelHandlerContext, promise: EventLoopPromise<Void>?) {
418418
guard context.handler is RemovableChannelHandler else {
419-
promise?.fail(ChannelError.unremovableHandler)
419+
promise?.fail(ChannelError._unremovableHandler)
420420
return
421421
}
422422
func removeHandler0() {
@@ -826,7 +826,7 @@ public final class ChannelPipeline: ChannelInvoker {
826826
if let firstOutboundCtx = firstOutboundCtx {
827827
firstOutboundCtx.invokeClose(mode: mode, promise: promise)
828828
} else {
829-
promise?.fail(ChannelError.alreadyClosed)
829+
promise?.fail(ChannelError._alreadyClosed)
830830
}
831831
}
832832

@@ -846,47 +846,47 @@ public final class ChannelPipeline: ChannelInvoker {
846846
if let firstOutboundCtx = firstOutboundCtx {
847847
firstOutboundCtx.invokeWrite(data, promise: promise)
848848
} else {
849-
promise?.fail(ChannelError.ioOnClosedChannel)
849+
promise?.fail(ChannelError._ioOnClosedChannel)
850850
}
851851
}
852852

853853
private func writeAndFlush0(_ data: NIOAny, promise: EventLoopPromise<Void>?) {
854854
if let firstOutboundCtx = firstOutboundCtx {
855855
firstOutboundCtx.invokeWriteAndFlush(data, promise: promise)
856856
} else {
857-
promise?.fail(ChannelError.ioOnClosedChannel)
857+
promise?.fail(ChannelError._ioOnClosedChannel)
858858
}
859859
}
860860

861861
private func bind0(to address: SocketAddress, promise: EventLoopPromise<Void>?) {
862862
if let firstOutboundCtx = firstOutboundCtx {
863863
firstOutboundCtx.invokeBind(to: address, promise: promise)
864864
} else {
865-
promise?.fail(ChannelError.ioOnClosedChannel)
865+
promise?.fail(ChannelError._ioOnClosedChannel)
866866
}
867867
}
868868

869869
private func connect0(to address: SocketAddress, promise: EventLoopPromise<Void>?) {
870870
if let firstOutboundCtx = firstOutboundCtx {
871871
firstOutboundCtx.invokeConnect(to: address, promise: promise)
872872
} else {
873-
promise?.fail(ChannelError.ioOnClosedChannel)
873+
promise?.fail(ChannelError._ioOnClosedChannel)
874874
}
875875
}
876876

877877
private func register0(promise: EventLoopPromise<Void>?) {
878878
if let firstOutboundCtx = firstOutboundCtx {
879879
firstOutboundCtx.invokeRegister(promise: promise)
880880
} else {
881-
promise?.fail(ChannelError.ioOnClosedChannel)
881+
promise?.fail(ChannelError._ioOnClosedChannel)
882882
}
883883
}
884884

885885
private func triggerUserOutboundEvent0(_ event: Any, promise: EventLoopPromise<Void>?) {
886886
if let firstOutboundCtx = firstOutboundCtx {
887887
firstOutboundCtx.invokeTriggerUserOutboundEvent(event, promise: promise)
888888
} else {
889-
promise?.fail(ChannelError.ioOnClosedChannel)
889+
promise?.fail(ChannelError._ioOnClosedChannel)
890890
}
891891
}
892892

@@ -1378,14 +1378,14 @@ extension ChannelPipeline.Position: Sendable {}
13781378

13791379
private extension CloseMode {
13801380
/// Returns the error to fail outstanding operations writes with.
1381-
var error: ChannelError {
1381+
var error: any Error {
13821382
switch self {
13831383
case .all:
1384-
return .ioOnClosedChannel
1384+
return ChannelError._ioOnClosedChannel
13851385
case .output:
1386-
return .outputClosed
1386+
return ChannelError._outputClosed
13871387
case .input:
1388-
return .inputClosed
1388+
return ChannelError._inputClosed
13891389
}
13901390
}
13911391
}
@@ -1577,7 +1577,7 @@ public final class ChannelHandlerContext: ChannelInvoker {
15771577
if let outboundNext = self.prev {
15781578
outboundNext.invokeRegister(promise: promise)
15791579
} else {
1580-
promise?.fail(ChannelError.ioOnClosedChannel)
1580+
promise?.fail(ChannelError._ioOnClosedChannel)
15811581
}
15821582
}
15831583

@@ -1591,7 +1591,7 @@ public final class ChannelHandlerContext: ChannelInvoker {
15911591
if let outboundNext = self.prev {
15921592
outboundNext.invokeBind(to: address, promise: promise)
15931593
} else {
1594-
promise?.fail(ChannelError.ioOnClosedChannel)
1594+
promise?.fail(ChannelError._ioOnClosedChannel)
15951595
}
15961596
}
15971597

@@ -1605,7 +1605,7 @@ public final class ChannelHandlerContext: ChannelInvoker {
16051605
if let outboundNext = self.prev {
16061606
outboundNext.invokeConnect(to: address, promise: promise)
16071607
} else {
1608-
promise?.fail(ChannelError.ioOnClosedChannel)
1608+
promise?.fail(ChannelError._ioOnClosedChannel)
16091609
}
16101610
}
16111611

@@ -1620,7 +1620,7 @@ public final class ChannelHandlerContext: ChannelInvoker {
16201620
if let outboundNext = self.prev {
16211621
outboundNext.invokeWrite(data, promise: promise)
16221622
} else {
1623-
promise?.fail(ChannelError.ioOnClosedChannel)
1623+
promise?.fail(ChannelError._ioOnClosedChannel)
16241624
}
16251625
}
16261626

@@ -1647,7 +1647,7 @@ public final class ChannelHandlerContext: ChannelInvoker {
16471647
if let outboundNext = self.prev {
16481648
outboundNext.invokeWriteAndFlush(data, promise: promise)
16491649
} else {
1650-
promise?.fail(ChannelError.ioOnClosedChannel)
1650+
promise?.fail(ChannelError._ioOnClosedChannel)
16511651
}
16521652
}
16531653

@@ -1671,7 +1671,7 @@ public final class ChannelHandlerContext: ChannelInvoker {
16711671
if let outboundNext = self.prev {
16721672
outboundNext.invokeClose(mode: mode, promise: promise)
16731673
} else {
1674-
promise?.fail(ChannelError.alreadyClosed)
1674+
promise?.fail(ChannelError._alreadyClosed)
16751675
}
16761676
}
16771677

@@ -1684,7 +1684,7 @@ public final class ChannelHandlerContext: ChannelInvoker {
16841684
if let outboundNext = self.prev {
16851685
outboundNext.invokeTriggerUserOutboundEvent(event, promise: promise)
16861686
} else {
1687-
promise?.fail(ChannelError.ioOnClosedChannel)
1687+
promise?.fail(ChannelError._ioOnClosedChannel)
16881688
}
16891689
}
16901690

Sources/NIOCore/DeadChannel.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,31 @@
1717
/// all operations.
1818
private final class DeadChannelCore: ChannelCore {
1919
func localAddress0() throws -> SocketAddress {
20-
throw ChannelError.ioOnClosedChannel
20+
throw ChannelError._ioOnClosedChannel
2121
}
2222

2323
func remoteAddress0() throws -> SocketAddress {
24-
throw ChannelError.ioOnClosedChannel
24+
throw ChannelError._ioOnClosedChannel
2525
}
2626

2727
func register0(promise: EventLoopPromise<Void>?) {
28-
promise?.fail(ChannelError.ioOnClosedChannel)
28+
promise?.fail(ChannelError._ioOnClosedChannel)
2929
}
3030

3131
func registerAlreadyConfigured0(promise: EventLoopPromise<Void>?) {
32-
promise?.fail(ChannelError.ioOnClosedChannel)
32+
promise?.fail(ChannelError._ioOnClosedChannel)
3333
}
3434

3535
func bind0(to: SocketAddress, promise: EventLoopPromise<Void>?) {
36-
promise?.fail(ChannelError.ioOnClosedChannel)
36+
promise?.fail(ChannelError._ioOnClosedChannel)
3737
}
3838

3939
func connect0(to: SocketAddress, promise: EventLoopPromise<Void>?) {
40-
promise?.fail(ChannelError.ioOnClosedChannel)
40+
promise?.fail(ChannelError._ioOnClosedChannel)
4141
}
4242

4343
func write0(_ data: NIOAny, promise: EventLoopPromise<Void>?) {
44-
promise?.fail(ChannelError.ioOnClosedChannel)
44+
promise?.fail(ChannelError._ioOnClosedChannel)
4545
}
4646

4747
func flush0() {
@@ -51,11 +51,11 @@ private final class DeadChannelCore: ChannelCore {
5151
}
5252

5353
func close0(error: Error, mode: CloseMode, promise: EventLoopPromise<Void>?) {
54-
promise?.fail(ChannelError.alreadyClosed)
54+
promise?.fail(ChannelError._alreadyClosed)
5555
}
5656

5757
func triggerUserOutboundEvent0(_ event: Any, promise: EventLoopPromise<Void>?) {
58-
promise?.fail(ChannelError.ioOnClosedChannel)
58+
promise?.fail(ChannelError._ioOnClosedChannel)
5959
}
6060

6161
func channelRead0(_ data: NIOAny) {
@@ -104,11 +104,11 @@ internal final class DeadChannel: Channel, @unchecked Sendable {
104104
let parent: Channel? = nil
105105

106106
func setOption<Option: ChannelOption>(_ option: Option, value: Option.Value) -> EventLoopFuture<Void> {
107-
return self.pipeline.eventLoop.makeFailedFuture(ChannelError.ioOnClosedChannel)
107+
return self.pipeline.eventLoop.makeFailedFuture(ChannelError._ioOnClosedChannel)
108108
}
109109

110110
func getOption<Option: ChannelOption>(_ option: Option) -> EventLoopFuture<Option.Value> {
111-
return eventLoop.makeFailedFuture(ChannelError.ioOnClosedChannel)
111+
return eventLoop.makeFailedFuture(ChannelError._ioOnClosedChannel)
112112
}
113113

114114
let isWritable = false

Sources/NIOCore/EventLoop.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public struct Scheduled<T> {
2626
@usableFromInline typealias CancelationCallback = @Sendable () -> Void
2727
/* private but usableFromInline */ @usableFromInline let _promise: EventLoopPromise<T>
2828
/* private but usableFromInline */ @usableFromInline let _cancellationTask: CancelationCallback
29-
29+
3030
@inlinable
3131
@preconcurrency
3232
public init(promise: EventLoopPromise<T>, cancellationTask: @escaping @Sendable () -> Void) {
@@ -40,7 +40,7 @@ public struct Scheduled<T> {
4040
/// This means that cancellation is not guaranteed.
4141
@inlinable
4242
public func cancel() {
43-
self._promise.fail(EventLoopError.cancelled)
43+
self._promise.fail(EventLoopError._cancelled)
4444
self._cancellationTask()
4545
}
4646

@@ -1219,6 +1219,11 @@ public enum EventLoopError: Error {
12191219
case shutdownFailed
12201220
}
12211221

1222+
extension EventLoopError {
1223+
@usableFromInline
1224+
static let _cancelled: any Error = EventLoopError.cancelled
1225+
}
1226+
12221227
extension EventLoopError: CustomStringConvertible {
12231228
public var description: String {
12241229
switch self {

Sources/NIOPosix/BSDSocketAPIPosix.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ extension NIOBSDSocket {
277277
option_value: &segmentSize,
278278
option_len: socklen_t(MemoryLayout<CInt>.size))
279279
#else
280-
throw ChannelError.operationUnsupported
280+
throw ChannelError._operationUnsupported
281281
#endif
282282
}
283283

@@ -294,7 +294,7 @@ extension NIOBSDSocket {
294294
}
295295
return segmentSize
296296
#else
297-
throw ChannelError.operationUnsupported
297+
throw ChannelError._operationUnsupported
298298
#endif
299299
}
300300

@@ -307,7 +307,7 @@ extension NIOBSDSocket {
307307
option_value: &isEnabled,
308308
option_len: socklen_t(MemoryLayout<CInt>.size))
309309
#else
310-
throw ChannelError.operationUnsupported
310+
throw ChannelError._operationUnsupported
311311
#endif
312312
}
313313

@@ -324,7 +324,7 @@ extension NIOBSDSocket {
324324
}
325325
return enabled != 0
326326
#else
327-
throw ChannelError.operationUnsupported
327+
throw ChannelError._operationUnsupported
328328
#endif
329329
}
330330
}

0 commit comments

Comments
 (0)