Skip to content

Commit 14b0a2d

Browse files
committed
Add benchmark suite.
_read coroutine on CircularBufferLineList subscript: Replaced get with _read + yield, so every _lines[index] access yields a borrowed reference instead of retaining/releasing the BufferLine. Reduces ARC overhead on the hot path. Direct array access in shiftElements: The loop now uses array[getCyclicIndex(...)] instead of self[...], bypassing the subscript entirely. Shifted elements are always populated, so the makeEmpty check is unnecessary here.
1 parent 8a5017d commit 14b0a2d

3 files changed

Lines changed: 84 additions & 39 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#if os(macOS)
2+
import Benchmark
3+
import Dispatch
4+
import Foundation
5+
import SwiftTerm
6+
7+
private enum SwiftTermBenchmarks {
8+
static let queue: DispatchQueue = {
9+
DispatchQueue(label: "Runner", qos: .userInteractive, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
10+
}()
11+
}
12+
13+
private func testFeed(benchmark: Benchmark, data: [UInt8], innerIterations: Int) {
14+
let h = HeadlessTerminal(queue: SwiftTermBenchmarks.queue) { _ in }
15+
let t = h.terminal!
16+
17+
t.feed(text: "\u{1b}[38;2;19;49;174;48;2;23;56;179mStringThis is a very long line\n\r")
18+
19+
benchmark.startMeasurement()
20+
for _ in benchmark.scaledIterations {
21+
for _ in 0..<innerIterations {
22+
t.feed(byteArray: data)
23+
}
24+
}
25+
benchmark.stopMeasurement()
26+
}
27+
28+
let benchmarks: @Sendable () -> Void = {
29+
Benchmark("testPerformance", configuration: .init(metrics: [.wallClock], maxDuration: .seconds(10))) { benchmark in
30+
let data = [UInt8]("pointless repetition\n".utf8)
31+
testFeed(benchmark: benchmark, data: data, innerIterations: 1_000)
32+
}
33+
34+
Benchmark("testPerformance2", configuration: .init(metrics: [.wallClock], maxDuration: .seconds(10))) { benchmark in
35+
let data = [UInt8]("pointless repetition\n".utf8)
36+
testFeed(benchmark: benchmark, data: data, innerIterations: 1_000)
37+
}
38+
}
39+
#else
40+
let benchmarks: @Sendable () -> Void = { }
41+
#endif

Package.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ let targets: [Target] = [
8080
name: "SwiftTermTests",
8181
dependencies: ["SwiftTerm"],
8282
path: "Tests/SwiftTermTests"
83+
),
84+
.executableTarget(
85+
name: "SwiftTermBenchmarks",
86+
dependencies: [
87+
"SwiftTerm",
88+
.product(name: "Benchmark", package: "package-benchmark")
89+
],
90+
path: "Benchmarks/SwiftTermBenchmarks",
91+
plugins: [
92+
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
93+
]
8394
)
8495
]
8596
#endif
@@ -96,6 +107,7 @@ let package = Package(
96107
dependencies: [
97108
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
98109
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.3"),
110+
.package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.29.11")),
99111
// .package(url: "https://github.com/swiftlang/swift-subprocess", revision: "426790f3f24afa60b418450da0afaa20a8b3bdd4")
100112
],
101113
targets: targets,

Sources/SwiftTerm/CircularList.swift

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class CircularList<T> {
3131
_count = newValue
3232
}
3333
}
34-
34+
3535
private var _count: Int
3636
var maxLength: Int {
3737
didSet {
@@ -54,23 +54,23 @@ class CircularList<T> {
5454
/// does not exist, or the index requested otherwise
5555
//
5656
var makeEmpty: ((_ idx: Int) -> T)? = nil
57-
57+
5858
public init (maxLength: Int)
5959
{
6060
array = Array.init(repeating: nil, count: Int(maxLength))
6161
self.maxLength = maxLength
6262
self._count = 0
6363
self.startIndex = 0
6464
}
65-
65+
6666
private func getCyclicIndex(_ index: Int) -> Int {
6767
return Int(startIndex + index) % (array.count)
6868
}
69-
69+
7070
func debugGetCyclicIndex(_ index: Int) -> Int {
7171
getCyclicIndex(index)
7272
}
73-
73+
7474
subscript (index: Int) -> T {
7575
get {
7676
let idx = getCyclicIndex(index)
@@ -87,7 +87,7 @@ class CircularList<T> {
8787
array [getCyclicIndex(index)] = newValue
8888
}
8989
}
90-
90+
9191
func push (_ value: T)
9292
{
9393
array [getCyclicIndex(count)] = value
@@ -109,17 +109,17 @@ class CircularList<T> {
109109
}
110110
let index = getCyclicIndex(count)
111111
startIndex += 1
112-
startIndex = startIndex % maxLength
112+
startIndex = startIndex % maxLength
113113
array [index] = makeEmpty! (-1)
114114
}
115-
115+
116116
@discardableResult
117117
func pop () -> T {
118118
let v = array [getCyclicIndex(count-1)]!
119119
count = count - 1
120120
return v
121121
}
122-
122+
123123
func splice (start: Int, deleteCount: Int, items: [T], change: (Int) -> Void)
124124
{
125125
if deleteCount > 0 {
@@ -147,7 +147,7 @@ class CircularList<T> {
147147
change(start + i)
148148
array [getCyclicIndex(start + i)] = items [i]
149149
}
150-
150+
151151
// Adjust length as needed
152152
if Int(count) + ic > array.count {
153153
let countToTrim = count + items.count - array.count
@@ -157,21 +157,21 @@ class CircularList<T> {
157157
count = count + items.count
158158
}
159159
}
160-
160+
161161
func trimStart (count: Int)
162162
{
163163
let c = count > self.count ? self.count : count
164164
startIndex = startIndex + c
165165
self.count -= count
166166
}
167-
167+
168168
func shiftElements (start: Int, count: Int, offset: Int) -> Bool
169169
{
170170
func dumpState (_ msg: String) -> Bool {
171171
print ("Assertion at start=\(start) count=\(count) offset=\(offset): \(msg)")
172172
return false
173173
}
174-
174+
175175
if count < 0 {
176176
return dumpState ("count < 0")
177177
}
@@ -208,7 +208,7 @@ class CircularList<T> {
208208
}
209209
return true
210210
}
211-
211+
212212
var isFull: Bool {
213213
get {
214214
return count == maxLength
@@ -272,36 +272,33 @@ internal class CircularBufferLineList {
272272

273273
/// Called when a line is pushed, with true if the line has images
274274
var onLinePushed: ((_ hasImages: Bool) -> Void)? = nil
275-
275+
276276
public init (maxLength: Int)
277277
{
278278
array = Array.init(repeating: nil, count: Int(maxLength))
279279
self.maxLength = maxLength
280280
self._count = 0
281281
self.startIndex = 0
282282
}
283-
283+
284284
/// The private version exists to allow the Swift optimizer to avoid calls to
285285
/// `swift_beginAccess`
286286
private func getCyclicIndex(_ index: Int) -> Int {
287287
return Int(startIndex &+ index) % (array.count)
288288
}
289-
289+
290290
/// Public version of the same method
291291
func debugGetCyclicIndex(_ index: Int) -> Int {
292292
return getCyclicIndex(index)
293293
}
294-
294+
295295
subscript (index: Int) -> BufferLine {
296-
get {
296+
_read {
297297
let idx = getCyclicIndex(index)
298-
if let p = array [idx] {
299-
return p
300-
} else {
301-
let new = makeEmpty! (idx)
302-
array [idx] = new
303-
return new
298+
if array[idx] == nil {
299+
array[idx] = makeEmpty!(idx)
304300
}
301+
yield array[idx]!
305302
}
306303
set (newValue){
307304
array [getCyclicIndex(index)] = newValue
@@ -336,14 +333,14 @@ internal class CircularBufferLineList {
336333
onLineRecycled?(hadImages)
337334
//array [index] = makeEmpty! (-1)
338335
}
339-
336+
340337
@discardableResult
341338
func pop () -> BufferLine {
342339
let v = array [getCyclicIndex(count-1)]!
343340
count = count - 1
344341
return v
345342
}
346-
343+
347344
func splice (start: Int, deleteCount: Int, items: [BufferLine], change: (Int) -> Void)
348345
{
349346
if deleteCount > 0 {
@@ -371,7 +368,7 @@ internal class CircularBufferLineList {
371368
change(start + i)
372369
array [getCyclicIndex(start + i)] = items [i]
373370
}
374-
371+
375372
// Adjust length as needed
376373
if Int(count) + ic > array.count {
377374
let countToTrim = count + items.count - array.count
@@ -381,21 +378,21 @@ internal class CircularBufferLineList {
381378
count = count + items.count
382379
}
383380
}
384-
381+
385382
func trimStart (count: Int)
386383
{
387384
let c = count > self.count ? self.count : count
388385
startIndex = startIndex + c
389386
self.count -= count
390387
}
391-
388+
392389
func shiftElements (start: Int, count: Int, offset: Int) -> Bool
393390
{
394391
func dumpState (_ msg: String) -> Bool {
395392
print ("Assertion at start=\(start) count=\(count) offset=\(offset): \(msg)")
396393
return false
397394
}
398-
395+
399396
if count < 0 {
400397
return dumpState ("count < 0")
401398
}
@@ -408,13 +405,9 @@ internal class CircularBufferLineList {
408405
if start+offset <= 0 {
409406
return dumpState ("start+offset <= 0")
410407
}
411-
// precondition (count > 0)
412-
// precondition (start >= 0)
413-
// precondition (start < self.count)
414-
// precondition (start+offset > 0)
415408
if offset > 0 {
416409
for i in (0..<count).reversed() {
417-
self [start + i + offset] = self [start + i]
410+
array[getCyclicIndex(start + i + offset)] = array[getCyclicIndex(start + i)]
418411
}
419412
let expandListBy = start + count + offset - self.count
420413
if expandListBy > 0 {
@@ -427,16 +420,15 @@ internal class CircularBufferLineList {
427420
}
428421
} else {
429422
for i in 0..<count {
430-
self [start + i + offset] = self [start + i]
423+
array[getCyclicIndex(start + i + offset)] = array[getCyclicIndex(start + i)]
431424
}
432425
}
433426
return true
434427
}
435-
428+
436429
var isFull: Bool {
437430
get {
438431
return count == maxLength
439432
}
440433
}
441434
}
442-

0 commit comments

Comments
 (0)