Skip to content

Commit b72fb0f

Browse files
authored
Improve performance of enumerating constraint-bound attributes consistent across many runs (#1226)
1 parent 8c87e9e commit b72fb0f

File tree

5 files changed

+105
-105
lines changed

5 files changed

+105
-105
lines changed

Benchmarks/Benchmarks/AttributedString/BenchmarkAttributedString.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,43 @@ let benchmarks = {
464464
blackHole(hasher.finalize())
465465
}
466466
#endif
467+
468+
let manyAttributesWithParagraph = {
469+
var str = createManyAttributesString()
470+
str.testParagraphConstrained = 2
471+
return str
472+
}()
473+
474+
Benchmark("paragraphBoundSliceEnumeration-shortRuns") { benchmark in
475+
for (value, range) in manyAttributesWithParagraph.runs[\.testParagraphConstrained] {
476+
blackHole(value)
477+
}
478+
}
479+
480+
Benchmark("paragraphBoundSliceEnumeration-shortRuns-reversed") { benchmark in
481+
for (value, range) in manyAttributesWithParagraph.runs[\.testParagraphConstrained].reversed() {
482+
blackHole(value)
483+
}
484+
}
485+
486+
let longParagraphsString = {
487+
var str = String(repeating: "a", count: 10000) + "\n"
488+
str += String(repeating: "b", count: 10000) + "\n"
489+
str += String(repeating: "c", count: 10000)
490+
return AttributedString(str, attributes: AttributeContainer.testParagraphConstrained(1).testInt(2))
491+
}()
492+
493+
Benchmark("paragraphBoundSliceEnumeration-longRuns") { benchmark in
494+
for (value, range) in longParagraphsString.runs[\.testParagraphConstrained] {
495+
blackHole(value)
496+
}
497+
}
498+
499+
Benchmark("paragraphBoundSliceEnumeration-longRuns-reversed") { benchmark in
500+
for (value, range) in longParagraphsString.runs[\.testParagraphConstrained].reversed() {
501+
blackHole(value)
502+
}
503+
}
467504
}
468505

469506

Sources/FoundationEssentials/AttributedString/AttributedString+Runs+AttributeSlices.swift

Lines changed: 13 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ extension AttributedString.Runs {
2424

2525
let runs: Runs
2626
let _names: [String]
27-
let _constraints: [AttributeRunBoundaries]
27+
let _constraints: Set<AttributeRunBoundaries?>
2828

2929
init(runs: Runs) {
3030
self.runs = runs
3131
// FIXME: ☠️ Get these from a proper cache in runs._guts.
3232
_names = [T.name]
33-
_constraints = T._constraintsInvolved
33+
_constraints = [T.runBoundaries]
3434
}
3535

3636
public struct Iterator: IteratorProtocol, Sendable {
@@ -162,13 +162,13 @@ extension AttributedString.Runs {
162162

163163
let runs : Runs
164164
let _names: [String]
165-
let _constraints: [AttributeRunBoundaries]
165+
let _constraints: Set<AttributeRunBoundaries?>
166166

167167
init(runs: Runs) {
168168
self.runs = runs
169169
// FIXME: ☠️ Get these from a proper cache in runs._guts.
170170
_names = [T.name, U.name]
171-
_constraints = Array(_contents: T.runBoundaries, U.runBoundaries)
171+
_constraints = [T.runBoundaries, U.runBoundaries]
172172
}
173173

174174
public struct Iterator: IteratorProtocol, Sendable {
@@ -320,13 +320,13 @@ extension AttributedString.Runs {
320320

321321
let runs : Runs
322322
let _names: [String]
323-
let _constraints: [AttributeRunBoundaries]
323+
let _constraints: Set<AttributeRunBoundaries?>
324324

325325
init(runs: Runs) {
326326
self.runs = runs
327327
// FIXME: ☠️ Get these from a proper cache in runs._guts.
328328
_names = [T.name, U.name, V.name]
329-
_constraints = Array(_contents: T.runBoundaries, U.runBoundaries, V.runBoundaries)
329+
_constraints = [T.runBoundaries, U.runBoundaries, V.runBoundaries]
330330
}
331331

332332
public struct Iterator: IteratorProtocol, Sendable {
@@ -490,14 +490,13 @@ extension AttributedString.Runs {
490490

491491
let runs : Runs
492492
let _names: [String]
493-
let _constraints: [AttributeRunBoundaries]
493+
let _constraints: Set<AttributeRunBoundaries?>
494494

495495
init(runs: Runs) {
496496
self.runs = runs
497497
// FIXME: ☠️ Get these from a proper cache in runs._guts.
498498
_names = [T.name, U.name, V.name, W.name]
499-
_constraints = Array(
500-
_contents: T.runBoundaries, U.runBoundaries, V.runBoundaries, W.runBoundaries)
499+
_constraints = [T.runBoundaries, U.runBoundaries, V.runBoundaries, W.runBoundaries]
501500
}
502501

503502
public struct Iterator: IteratorProtocol, Sendable {
@@ -675,18 +674,19 @@ extension AttributedString.Runs {
675674

676675
let runs : Runs
677676
let _names: [String]
678-
let _constraints: [AttributeRunBoundaries]
677+
let _constraints: Set<AttributeRunBoundaries?>
679678

680679
init(runs: Runs) {
681680
self.runs = runs
682681
// FIXME: ☠️ Get these from a proper cache in runs._guts.
683682
_names = [T.name, U.name, V.name, W.name]
684-
_constraints = Array(
685-
_contents: T.runBoundaries,
683+
_constraints = [
684+
T.runBoundaries,
686685
U.runBoundaries,
687686
V.runBoundaries,
688687
W.runBoundaries,
689-
X.runBoundaries)
688+
X.runBoundaries
689+
]
690690
}
691691

692692
public struct Iterator: IteratorProtocol, Sendable {
@@ -955,75 +955,3 @@ extension AttributedString.Runs {
955955
}
956956

957957
#endif // FOUNDATION_FRAMEWORK
958-
959-
extension RangeReplaceableCollection {
960-
internal init(_contents item1: Element?) {
961-
self.init()
962-
if let item1 { self.append(item1) }
963-
}
964-
965-
internal init(_contents item1: Element?, _ item2: Element?) {
966-
self.init()
967-
var c = 0
968-
if item1 != nil { c &+= 1 }
969-
if item2 != nil { c &+= 1 }
970-
guard c > 0 else { return }
971-
self.reserveCapacity(c)
972-
if let item1 { self.append(item1) }
973-
if let item2 { self.append(item2) }
974-
}
975-
976-
internal init(_contents item1: Element?, _ item2: Element?, _ item3: Element?) {
977-
self.init()
978-
var c = 0
979-
if item1 != nil { c &+= 1 }
980-
if item2 != nil { c &+= 1 }
981-
if item3 != nil { c &+= 1 }
982-
guard c > 0 else { return }
983-
self.reserveCapacity(c)
984-
if let item1 { self.append(item1) }
985-
if let item2 { self.append(item2) }
986-
if let item3 { self.append(item3) }
987-
}
988-
989-
internal init(
990-
_contents item1: Element?, _ item2: Element?, _ item3: Element?, _ item4: Element?
991-
) {
992-
self.init()
993-
var c = 0
994-
if item1 != nil { c &+= 1 }
995-
if item2 != nil { c &+= 1 }
996-
if item3 != nil { c &+= 1 }
997-
if item4 != nil { c &+= 1 }
998-
guard c > 0 else { return }
999-
self.reserveCapacity(c)
1000-
if let item1 { self.append(item1) }
1001-
if let item2 { self.append(item2) }
1002-
if let item3 { self.append(item3) }
1003-
if let item4 { self.append(item4) }
1004-
}
1005-
1006-
internal init(
1007-
_contents item1: Element?,
1008-
_ item2: Element?,
1009-
_ item3: Element?,
1010-
_ item4: Element?,
1011-
_ item5: Element?
1012-
) {
1013-
self.init()
1014-
var c = 0
1015-
if item1 != nil { c &+= 1 }
1016-
if item2 != nil { c &+= 1 }
1017-
if item3 != nil { c &+= 1 }
1018-
if item4 != nil { c &+= 1 }
1019-
if item5 != nil { c &+= 1 }
1020-
guard c > 0 else { return }
1021-
self.reserveCapacity(c)
1022-
if let item1 { self.append(item1) }
1023-
if let item2 { self.append(item2) }
1024-
if let item3 { self.append(item3) }
1025-
if let item4 { self.append(item4) }
1026-
if let item5 { self.append(item5) }
1027-
}
1028-
}
1029-

Sources/FoundationEssentials/AttributedString/AttributedString+Runs.swift

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -481,19 +481,36 @@ extension AttributedString.Runs {
481481
internal func _slicedRunBoundary(
482482
after i: AttributedString.Index,
483483
attributeNames: [String],
484-
constraints: [AttributeRunBoundaries],
484+
constraints: Set<AttributeRunBoundaries?>,
485485
endOfCurrent: Bool
486486
) -> AttributedString.Index {
487487
precondition(
488488
self._strBounds.contains(i._value),
489489
"AttributedString index is out of bounds")
490490
precondition(!attributeNames.isEmpty)
491491
let r = _guts.findRun(at: i._value)
492+
let currentRangeIdx = _strBounds.rangeIdx(containing: i._value)
493+
let currentRange = _strBounds.ranges[currentRangeIdx]
494+
495+
guard constraints.count != 1 || constraints.contains(nil) else {
496+
// We have a single constraint and attributes are guaranteed to be consistent between constraint boundaries
497+
// This means that we will not break until the next constraint boundary, so we don't need to enumerate the actual run contents
498+
let constraintBreak = _guts.string._firstConstraintBreak(in: i._value ..< currentRange.upperBound, with: constraints)
499+
if !endOfCurrent && constraintBreak == currentRange.upperBound {
500+
// No constraint break, return the next subrange start or the end index
501+
if currentRangeIdx == _strBounds.ranges.count - 1 {
502+
return .init(currentRange.upperBound, version: _guts.version)
503+
} else {
504+
return .init(_strBounds.ranges[currentRangeIdx + 1].lowerBound, version: _guts.version)
505+
}
506+
} else {
507+
return .init(constraintBreak, version: _guts.version)
508+
}
509+
}
510+
492511
let endRun = _lastOfMatchingRuns(with: r.runIndex, comparing: attributeNames)
493512
let utf8End = endRun.utf8Offset + _guts.runs[endRun].length
494513
let strIndexEnd = _guts.string.utf8.index(r.start, offsetBy: utf8End - r.start.utf8Offset)
495-
let currentRangeIdx = _strBounds.rangeIdx(containing: i._value)
496-
let currentRange = _strBounds.ranges[currentRangeIdx]
497514
if strIndexEnd < currentRange.upperBound {
498515
// The coalesced run ends within the current range, so just look for the next break in the coalesced run
499516
return .init(_guts.string._firstConstraintBreak(in: i._value ..< strIndexEnd, with: constraints), version: _guts.version)
@@ -520,7 +537,7 @@ extension AttributedString.Runs {
520537
internal func _slicedRunBoundary(
521538
before i: AttributedString.Index,
522539
attributeNames: [String],
523-
constraints: [AttributeRunBoundaries],
540+
constraints: Set<AttributeRunBoundaries?>,
524541
endOfPrevious: Bool
525542
) -> AttributedString.Index {
526543
precondition(
@@ -545,6 +562,11 @@ extension AttributedString.Runs {
545562
currentStringIdx = currentRange.upperBound
546563
if endOfPrevious { return .init(currentStringIdx, version: _guts.version) }
547564
}
565+
566+
guard constraints.count != 1 || constraints.contains(nil) else {
567+
return .init(_guts.string._lastConstraintBreak(in: currentRange.lowerBound ..< currentStringIdx, with: constraints), version: _guts.version)
568+
}
569+
548570
let beforeStringIdx = _guts.string.utf8.index(before: currentStringIdx)
549571
let r = _guts.runs.index(atUTF8Offset: beforeStringIdx.utf8Offset)
550572
let startRun = _firstOfMatchingRuns(with: r.index, comparing: attributeNames)
@@ -561,7 +583,7 @@ extension AttributedString.Runs {
561583
internal func _slicedRunBoundary(
562584
roundingDown i: AttributedString.Index,
563585
attributeNames: [String],
564-
constraints: [AttributeRunBoundaries]
586+
constraints: Set<AttributeRunBoundaries?>
565587
) -> (index: AttributedString.Index, runIndex: AttributedString._InternalRuns.Index) {
566588
precondition(
567589
_strBounds.contains(i._value) || i._value == endIndex._stringIndex,
@@ -571,8 +593,22 @@ extension AttributedString.Runs {
571593
if r.runIndex.offset == endIndex._runOffset {
572594
return (i, r.runIndex)
573595
}
574-
let startRun = _firstOfMatchingRuns(with: r.runIndex, comparing: attributeNames)
575596
let currentRange = _strBounds.ranges[_strBounds.rangeIdx(containing: i._value)]
597+
598+
guard constraints.count != 1 || constraints.contains(nil) else {
599+
let nextIndex = _guts.string.unicodeScalars.index(after: i._value)
600+
let constraintBreak = _guts.string._lastConstraintBreak(in: currentRange.lowerBound ..< nextIndex, with: constraints)
601+
var runIdx = r.runIndex
602+
while runIdx.utf8Offset > constraintBreak.utf8Offset {
603+
_guts.runs.formIndex(before: &runIdx)
604+
}
605+
return (
606+
.init(constraintBreak, version: _guts.version),
607+
runIdx
608+
)
609+
}
610+
611+
let startRun = _firstOfMatchingRuns(with: r.runIndex, comparing: attributeNames)
576612
let stringStart = Swift.max(
577613
_guts.string.utf8.index(r.start, offsetBy: startRun.utf8Offset - r.start.utf8Offset),
578614
currentRange.lowerBound)
@@ -587,9 +623,9 @@ extension AttributedString.Runs {
587623
extension BigString {
588624
internal func _firstConstraintBreak(
589625
in range: Range<Index>,
590-
with constraints: [AttributedString.AttributeRunBoundaries]
626+
with constraints: Set<AttributedString.AttributeRunBoundaries?>
591627
) -> Index {
592-
guard !constraints.isEmpty, !range.isEmpty else { return range.upperBound }
628+
guard !range.isEmpty else { return range.upperBound }
593629

594630
var r = range
595631
if
@@ -602,7 +638,7 @@ extension BigString {
602638
if constraints._containsScalarConstraint {
603639
// Note: we need to slice runs on matching scalars even if they don't carry
604640
// the attributes we're looking for.
605-
let scalars: [UnicodeScalar] = constraints.compactMap { $0._constrainedScalar }
641+
let scalars: [UnicodeScalar] = constraints.compactMap { $0?._constrainedScalar }
606642
if let firstBreak = self.unicodeScalars[r]._findFirstScalarBoundary(for: scalars) {
607643
r = r.lowerBound ..< firstBreak
608644
}
@@ -613,9 +649,9 @@ extension BigString {
613649

614650
internal func _lastConstraintBreak(
615651
in range: Range<Index>,
616-
with constraints: [AttributedString.AttributeRunBoundaries]
652+
with constraints: Set<AttributedString.AttributeRunBoundaries?>
617653
) -> Index {
618-
guard !constraints.isEmpty, !range.isEmpty else { return range.lowerBound }
654+
guard !range.isEmpty else { return range.lowerBound }
619655

620656
var r = range
621657
if
@@ -628,7 +664,7 @@ extension BigString {
628664
if constraints._containsScalarConstraint {
629665
// Note: we need to slice runs on matching scalars even if they don't carry
630666
// the attributes we're looking for.
631-
let scalars: [UnicodeScalar] = constraints.compactMap { $0._constrainedScalar }
667+
let scalars: [UnicodeScalar] = constraints.compactMap { $0?._constrainedScalar }
632668
if let lastBreak = self.unicodeScalars[r]._findLastScalarBoundary(for: scalars) {
633669
r = lastBreak ..< r.upperBound
634670
}

Sources/FoundationEssentials/AttributedString/AttributedStringAttribute.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,6 @@ extension AttributedStringKey {
123123
public static var invalidationConditions : Set<AttributedString.AttributeInvalidationCondition>? { nil }
124124
}
125125

126-
extension AttributedStringKey {
127-
// FIXME: ☠️ Allocating an Array here is not a good idea.
128-
static var _constraintsInvolved: [AttributedString.AttributeRunBoundaries] {
129-
guard let rb = runBoundaries else { return [] }
130-
return [rb]
131-
}
132-
}
133-
134126
// MARK: Attribute Scopes
135127

136128
@dynamicMemberLookup @frozen

Sources/FoundationEssentials/AttributedString/AttributedStringAttributeConstrainingBehavior.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ extension Collection where Element == AttributedString.AttributeRunBoundaries {
8383
}
8484
}
8585

86+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
87+
extension Collection where Element == AttributedString.AttributeRunBoundaries? {
88+
var _containsScalarConstraint: Bool {
89+
self.contains { $0?._isScalarConstrained ?? false }
90+
}
91+
}
92+
8693
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
8794
extension AttributedString.Guts {
8895

0 commit comments

Comments
 (0)