Skip to content

Commit 076fda1

Browse files
authored
Use CircularBuffer in EmbeddedChannel. (#1700)
Motivation: When running load through EmbeddedChannel we spend an enormous amount of time screwing around with removing things from Arrays. Arrays are not a natural data type for `removeFirst()`, and in fact that method is linear-time on Array due to the need for Array to be zero-indexed. Let's stop using (and indeed misusing) Array on EmbeddedChannel. While we're here, if we add some judicious @inlinable annotations we can also save additional work generating results that users don't need. Modifications: - Replace arrays with circular buffers (including marked versions). - Avoid CoWs and extra allocations on flush. - Make some API methods inlinable to make them cheaper. Result: - Much cheaper EmbeddedChannel for benchmark purposes.
1 parent 2bae395 commit 076fda1

File tree

5 files changed

+61
-38
lines changed

5 files changed

+61
-38
lines changed

Sources/NIO/Embedded.swift

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ public final class EmbeddedEventLoop: EventLoop {
193193
}
194194
}
195195

196+
@usableFromInline
196197
class EmbeddedChannelCore: ChannelCore {
197198
var isOpen: Bool = true
198199
var isActive: Bool = false
@@ -217,23 +218,29 @@ class EmbeddedChannelCore: ChannelCore {
217218
}
218219

219220
/// Contains the flushed items that went into the `Channel` (and on a regular channel would have hit the network).
220-
var outboundBuffer: [NIOAny] = []
221+
@usableFromInline
222+
var outboundBuffer: CircularBuffer<NIOAny> = CircularBuffer()
221223

222224
/// Contains the unflushed items that went into the `Channel`
223-
var pendingOutboundBuffer: [(NIOAny, EventLoopPromise<Void>?)] = []
225+
@usableFromInline
226+
var pendingOutboundBuffer: MarkedCircularBuffer<(NIOAny, EventLoopPromise<Void>?)> = MarkedCircularBuffer(initialCapacity: 16)
224227

225228
/// Contains the items that travelled the `ChannelPipeline` all the way and hit the tail channel handler. On a
226229
/// regular `Channel` these items would be lost.
227-
var inboundBuffer: [NIOAny] = []
230+
@usableFromInline
231+
var inboundBuffer: CircularBuffer<NIOAny> = CircularBuffer()
228232

233+
@usableFromInline
229234
func localAddress0() throws -> SocketAddress {
230235
throw ChannelError.operationUnsupported
231236
}
232237

238+
@usableFromInline
233239
func remoteAddress0() throws -> SocketAddress {
234240
throw ChannelError.operationUnsupported
235241
}
236242

243+
@usableFromInline
237244
func close0(error: Error, mode: CloseMode, promise: EventLoopPromise<Void>?) {
238245
guard self.isOpen else {
239246
promise?.fail(ChannelError.alreadyClosed)
@@ -254,43 +261,47 @@ class EmbeddedChannelCore: ChannelCore {
254261
}
255262
}
256263

264+
@usableFromInline
257265
func bind0(to address: SocketAddress, promise: EventLoopPromise<Void>?) {
258266
promise?.succeed(())
259267
}
260268

269+
@usableFromInline
261270
func connect0(to address: SocketAddress, promise: EventLoopPromise<Void>?) {
262271
isActive = true
263272
promise?.succeed(())
264273
pipeline.fireChannelActive0()
265274
}
266275

276+
@usableFromInline
267277
func register0(promise: EventLoopPromise<Void>?) {
268278
promise?.succeed(())
269279
pipeline.fireChannelRegistered0()
270280
}
271281

282+
@usableFromInline
272283
func registerAlreadyConfigured0(promise: EventLoopPromise<Void>?) {
273284
isActive = true
274285
register0(promise: promise)
275286
pipeline.fireChannelActive0()
276287
}
277288

289+
@usableFromInline
278290
func write0(_ data: NIOAny, promise: EventLoopPromise<Void>?) {
279291
self.pendingOutboundBuffer.append((data, promise))
280292
}
281293

294+
@usableFromInline
282295
func flush0() {
283-
let pendings = self.pendingOutboundBuffer
284-
// removeAll(keepingCapacity:) is strictly more expensive than doing this, see
285-
// https://bugs.swift.org/browse/SR-13923.
286-
self.pendingOutboundBuffer = []
287-
self.pendingOutboundBuffer.reserveCapacity(pendings.capacity)
288-
for dataAndPromise in pendings {
296+
self.pendingOutboundBuffer.mark()
297+
298+
while self.pendingOutboundBuffer.hasMark, let dataAndPromise = self.pendingOutboundBuffer.popFirst() {
289299
self.addToBuffer(buffer: &self.outboundBuffer, data: dataAndPromise.0)
290300
dataAndPromise.1?.succeed(())
291301
}
292302
}
293303

304+
@usableFromInline
294305
func read0() {
295306
// NOOP
296307
}
@@ -299,6 +310,7 @@ class EmbeddedChannelCore: ChannelCore {
299310
promise?.fail(ChannelError.operationUnsupported)
300311
}
301312

313+
@usableFromInline
302314
func channelRead0(_ data: NIOAny) {
303315
addToBuffer(buffer: &inboundBuffer, data: data)
304316
}
@@ -309,7 +321,7 @@ class EmbeddedChannelCore: ChannelCore {
309321
}
310322
}
311323

312-
private func addToBuffer<T>(buffer: inout [T], data: T) {
324+
private func addToBuffer<T>(buffer: inout CircularBuffer<T>, data: T) {
313325
buffer.append(data)
314326
}
315327
}
@@ -409,6 +421,11 @@ public final class EmbeddedChannel: Channel {
409421
/// The type of the actual first element.
410422
public let actual: Any.Type
411423

424+
public init(expected: Any.Type, actual: Any.Type) {
425+
self.expected = expected
426+
self.actual = actual
427+
}
428+
412429
public static func == (lhs: WrongTypeError, rhs: WrongTypeError) -> Bool {
413430
return lhs.expected == rhs.expected && lhs.actual == rhs.actual
414431
}
@@ -424,7 +441,8 @@ public final class EmbeddedChannel: Channel {
424441
/// - see: `Channel.closeFuture`
425442
public var closeFuture: EventLoopFuture<Void> { return channelcore.closePromise.futureResult }
426443

427-
private lazy var channelcore: EmbeddedChannelCore = EmbeddedChannelCore(pipeline: self._pipeline, eventLoop: self.eventLoop)
444+
@usableFromInline
445+
/*private but usableFromInline */ lazy var channelcore: EmbeddedChannelCore = EmbeddedChannelCore(pipeline: self._pipeline, eventLoop: self.eventLoop)
428446

429447
/// - see: `Channel._channelCore`
430448
public var _channelCore: ChannelCore {
@@ -464,8 +482,8 @@ public final class EmbeddedChannel: Channel {
464482
if c.outboundBuffer.isEmpty && c.inboundBuffer.isEmpty && c.pendingOutboundBuffer.isEmpty {
465483
return .clean
466484
} else {
467-
return .leftOvers(inbound: c.inboundBuffer,
468-
outbound: c.outboundBuffer,
485+
return .leftOvers(inbound: Array(c.inboundBuffer),
486+
outbound: Array(c.outboundBuffer),
469487
pendingOutbound: c.pendingOutboundBuffer.map { $0.0 })
470488
}
471489
}
@@ -518,8 +536,9 @@ public final class EmbeddedChannel: Channel {
518536
/// - note: Outbound events travel the `ChannelPipeline` _back to front_.
519537
/// - note: `EmbeddedChannel.writeOutbound` will `write` data through the `ChannelPipeline`, starting with last
520538
/// `ChannelHandler`.
539+
@inlinable
521540
public func readOutbound<T>(as type: T.Type = T.self) throws -> T? {
522-
return try readFromBuffer(buffer: &channelcore.outboundBuffer)
541+
return try _readFromBuffer(buffer: &channelcore.outboundBuffer)
523542
}
524543

525544
/// If available, this method reads one element of type `T` out of the `EmbeddedChannel`'s inbound buffer. If the
@@ -532,8 +551,9 @@ public final class EmbeddedChannel: Channel {
532551
/// `ChannelHandlerContext.fireChannelRead`) or implicitly by not implementing `channelRead`.
533552
///
534553
/// - note: `EmbeddedChannel.writeInbound` will fire data through the `ChannelPipeline` using `fireChannelRead`.
554+
@inlinable
535555
public func readInbound<T>(as type: T.Type = T.self) throws -> T? {
536-
return try readFromBuffer(buffer: &channelcore.inboundBuffer)
556+
return try _readFromBuffer(buffer: &channelcore.inboundBuffer)
537557
}
538558

539559
/// Sends an inbound `channelRead` event followed by a `channelReadComplete` event through the `ChannelPipeline`.
@@ -545,11 +565,12 @@ public final class EmbeddedChannel: Channel {
545565
/// - data: The data to fire through the pipeline.
546566
/// - returns: The state of the inbound buffer which contains all the events that travelled the `ChannelPipeline`
547567
// all the way.
568+
@inlinable
548569
@discardableResult public func writeInbound<T>(_ data: T) throws -> BufferState {
549570
pipeline.fireChannelRead(NIOAny(data))
550571
pipeline.fireChannelReadComplete()
551572
try throwIfErrorCaught()
552-
return self.channelcore.inboundBuffer.isEmpty ? .empty : .full(self.channelcore.inboundBuffer)
573+
return self.channelcore.inboundBuffer.isEmpty ? .empty : .full(Array(self.channelcore.inboundBuffer))
553574
}
554575

555576
/// Sends an outbound `writeAndFlush` event through the `ChannelPipeline`.
@@ -562,9 +583,10 @@ public final class EmbeddedChannel: Channel {
562583
/// - data: The data to fire through the pipeline.
563584
/// - returns: The state of the outbound buffer which contains all the events that travelled the `ChannelPipeline`
564585
// all the way.
586+
@inlinable
565587
@discardableResult public func writeOutbound<T>(_ data: T) throws -> BufferState {
566588
try writeAndFlush(NIOAny(data)).wait()
567-
return self.channelcore.outboundBuffer.isEmpty ? .empty : .full(self.channelcore.outboundBuffer)
589+
return self.channelcore.outboundBuffer.isEmpty ? .empty : .full(Array(self.channelcore.outboundBuffer))
568590
}
569591

570592
/// This method will throw the error that is stored in the `EmbeddedChannel` if any.
@@ -577,7 +599,8 @@ public final class EmbeddedChannel: Channel {
577599
}
578600
}
579601

580-
private func readFromBuffer<T>(buffer: inout [NIOAny]) throws -> T? {
602+
@inlinable
603+
func _readFromBuffer<T>(buffer: inout CircularBuffer<NIOAny>) throws -> T? {
581604
if buffer.isEmpty {
582605
return nil
583606
}

docker/docker-compose.1604.52.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ services:
2626
- MAX_ALLOCS_ALLOWED_creating_10000_headers=100 # 5.2 improvement 10000
2727
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
2828
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
29-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer=1010
30-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer_with_space=1010
31-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=4010
32-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=4010
33-
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=1000
29+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer=1000
30+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer_with_space=1000
31+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=5010
32+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=5010
33+
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
3434
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=2010 # 5.2 improvement 4000
3535
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
3636
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=210050

docker/docker-compose.1604.53.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ services:
2626
- MAX_ALLOCS_ALLOWED_creating_10000_headers=100
2727
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
2828
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
29-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer=1010
30-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer_with_space=1010
31-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=4010
32-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=4010
33-
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=1000
29+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer=1000
30+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer_with_space=1000
31+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=5010
32+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=5010
33+
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
3434
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=2010
3535
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
3636
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=200500 #5.3 improvement 210050

docker/docker-compose.1804.50.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ services:
2626
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
2727
- MAX_ALLOCS_ALLOWED_creating_10000_headers=10100
2828
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
29-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer=1010
30-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer_with_space=1010
31-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=4010
32-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=4010
33-
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=1000
29+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer=1000
30+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer_with_space=1000
31+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=5010
32+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=5010
33+
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
3434
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=6010
3535
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
3636
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=230050

docker/docker-compose.1804.51.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ services:
2626
- MAX_ALLOCS_ALLOWED_creating_10000_headers=10100
2727
- MAX_ALLOCS_ALLOWED_scheduling_10000_executions=20150
2828
- MAX_ALLOCS_ALLOWED_modifying_1000_circular_buffer_elements=50
29-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer=1010
30-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer_with_space=1010
31-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=4010
32-
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=4010
33-
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=1000
29+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer=1000
30+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_holding_buffer_with_space=1000
31+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=5010
32+
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=5010
33+
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=2000
3434
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=6010
3535
- MAX_ALLOCS_ALLOWED_schedule_10000_tasks=90050
3636
- MAX_ALLOCS_ALLOWED_read_10000_chunks_from_file=210050

0 commit comments

Comments
 (0)