Skip to content

Commit 9f3a180

Browse files
authored
Improve performance AttributedString Equatable conformance (#1287)
1 parent eb7f240 commit 9f3a180

File tree

2 files changed

+58
-69
lines changed

2 files changed

+58
-69
lines changed

Sources/FoundationEssentials/AttributedString/AttributedString+Runs.swift

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,14 @@ extension AttributedString.Runs: BidirectionalCollection {
278278
precondition(i < _bounds.upperBound, "Can't advance AttributedString.Runs index beyond end")
279279
let (resolvedIdx, runStartIdx) = _resolve(i)
280280
let next = _guts.runs.index(after: resolvedIdx)
281-
let currentRangeIdx = i._rangesOffset ?? _strBounds.rangeIdx(containing: i._stringIndex ?? runStartIdx)
282-
let currentRange = _strBounds.ranges[currentRangeIdx]
281+
let currentRangeIdx: Int
282+
let currentRange: Range<BigString.Index>
283+
if let cachedRangeOffset = i._rangesOffset {
284+
currentRangeIdx = cachedRangeOffset
285+
currentRange = _strBounds.ranges[currentRangeIdx]
286+
} else {
287+
(currentRange, currentRangeIdx) = _strBounds.range(containing: i._stringIndex ?? runStartIdx)
288+
}
283289
if currentRange.upperBound.utf8Offset <= next.utf8Offset {
284290
let nextRangeIdx = currentRangeIdx + 1
285291
if nextRangeIdx == _strBounds.ranges.count {
@@ -300,7 +306,7 @@ extension AttributedString.Runs: BidirectionalCollection {
300306
public func index(before i: Index) -> Index {
301307
precondition(i > _bounds.lowerBound, "Can't step AttributedString.Runs index below start")
302308
let (resolvedIdx, runStartIdx) = _resolve(i)
303-
let currentRangeIdx = i._rangesOffset ?? _strBounds.rangeIdx(containing: i._stringIndex ?? runStartIdx)
309+
let currentRangeIdx = i._rangesOffset ?? _strBounds.range(containing: i._stringIndex ?? runStartIdx).offset
304310
if i == endIndex || runStartIdx.utf8Offset <= _strBounds.ranges[currentRangeIdx].lowerBound.utf8Offset {
305311
// The current run starts on or before our current range, look up the next range
306312
let previousRange = _strBounds.ranges[currentRangeIdx - 1]
@@ -394,27 +400,34 @@ extension AttributedString.Runs: BidirectionalCollection {
394400

395401
public subscript(position: Index) -> Run {
396402
precondition(_bounds.contains(position), "AttributedString.Runs index is out of bounds")
397-
if let strIdx = position._stringIndex {
398-
precondition(_strBounds.contains(strIdx), "AttributedString.Runs index is out of bounds")
399-
}
400403
let resolved = _resolve(position)
401-
return self[_unchecked: resolved.runIndex, stringStartIdx: position._startStringIndex ?? resolved.start, stringIdx: position._stringIndex ?? resolved.start, rangeOffset: position._rangesOffset]
404+
let containingRange: Range<BigString.Index>
405+
let stringIdx: BigString.Index
406+
if let cachedRangeOffset = position._rangesOffset {
407+
containingRange = _strBounds.ranges[cachedRangeOffset]
408+
if let cachedStringIdx = position._stringIndex {
409+
precondition(containingRange.contains(cachedStringIdx), "AttributedString.Runs index is out of bounds")
410+
stringIdx = cachedStringIdx
411+
}
412+
} else {
413+
stringIdx = position._stringIndex ?? resolved.start
414+
// No need to check that _strBounds contains stringIdx here, the below call will assert if it cannot find a range that contains the provided index
415+
containingRange = _strBounds.range(containing: stringIdx).range
416+
}
417+
return self[_unchecked: resolved.runIndex, stringStartIdx: position._startStringIndex ?? resolved.start, stringIdx: position._stringIndex ?? resolved.start, containingRange: containingRange]
402418
}
403419

404420
public subscript(position: AttributedString.Index) -> Run {
405-
precondition(
406-
_strBounds.contains(position._value),
407-
"AttributedString index is out of bounds")
421+
let containingRange = _strBounds.range(containing: position._value).range
408422
let r = _guts.findRun(at: position._value)
409-
return self[_unchecked: r.runIndex, stringStartIdx: r.start, stringIdx: position._value]
423+
return self[_unchecked: r.runIndex, stringStartIdx: r.start, stringIdx: position._value, containingRange: containingRange]
410424
}
411425

412-
internal subscript(_unchecked i: _InternalRuns.Index, stringStartIdx stringStartIdx: BigString.Index, stringIdx stringIdx: BigString.Index, rangeOffset rangeOffset: Int? = nil) -> Run {
426+
internal subscript(_unchecked i: _InternalRuns.Index, stringStartIdx stringStartIdx: BigString.Index, stringIdx stringIdx: BigString.Index, containingRange containingRange: Range<BigString.Index>) -> Run {
413427
let run = _guts.runs[i]
414428
// Clamp the run into the bounds of self, using relative calculations.
415-
let range = _strBounds.ranges[rangeOffset ?? _strBounds.rangeIdx(containing: stringIdx)]
416-
let lowerBound = Swift.max(stringStartIdx, range.lowerBound)
417-
let upperUTF8 = Swift.min(stringStartIdx.utf8Offset + run.length, range.upperBound.utf8Offset)
429+
let lowerBound = Swift.max(stringStartIdx, containingRange.lowerBound)
430+
let upperUTF8 = Swift.min(stringStartIdx.utf8Offset + run.length, containingRange.upperBound.utf8Offset)
418431
let upperBound = _guts.string.utf8.index(stringIdx, offsetBy: upperUTF8 - stringIdx.utf8Offset)
419432
return Run(_attributes: run.attributes, Range(uncheckedBounds: (lowerBound, upperBound)), _guts)
420433
}
@@ -428,8 +441,7 @@ extension AttributedString.Runs {
428441
_strBounds.contains(position._value),
429442
"AttributedString index is out of bounds")
430443
let r = _guts.findRun(at: position._value)
431-
let rangeIdx = _strBounds.rangeIdx(containing: position._value)
432-
let range = _strBounds.ranges[rangeIdx]
444+
let (range, rangeIdx) = _strBounds.range(containing: position._value)
433445
let strIdx = Swift.max(range.lowerBound, r.start)
434446
return Index(_runIndex: r.runIndex, startStringIndex: r.start, stringIndex: strIdx, rangeOffset: rangeIdx, withinDiscontiguous: _isDiscontiguous)
435447
}
@@ -484,13 +496,10 @@ extension AttributedString.Runs {
484496
constraints: Set<AttributeRunBoundaries?>,
485497
endOfCurrent: Bool
486498
) -> AttributedString.Index {
487-
precondition(
488-
self._strBounds.contains(i._value),
489-
"AttributedString index is out of bounds")
499+
// _strBounds.range(containing:) below validates that i._value is within the bounds of this slice
490500
precondition(!attributeNames.isEmpty)
491501
let r = _guts.findRun(at: i._value)
492-
let currentRangeIdx = _strBounds.rangeIdx(containing: i._value)
493-
let currentRange = _strBounds.ranges[currentRangeIdx]
502+
let (currentRange, currentRangeIdx) = _strBounds.range(containing: i._value)
494503

495504
guard constraints.count != 1 || constraints.contains(nil) else {
496505
// We have a single constraint and attributes are guaranteed to be consistent between constraint boundaries
@@ -540,18 +549,15 @@ extension AttributedString.Runs {
540549
constraints: Set<AttributeRunBoundaries?>,
541550
endOfPrevious: Bool
542551
) -> AttributedString.Index {
543-
precondition(
544-
_strBounds.contains(i._value) || i._value == endIndex._stringIndex,
545-
"AttributedString index is out of bounds")
552+
// _strBounds.range(containing:) below validates that i._value is within the bounds of this slice
546553
precondition(!attributeNames.isEmpty)
547554
var currentRangeIdx: Int
548555
var currentRange: Range<BigString.Index>
549556
if i._value == endIndex._stringIndex {
550557
currentRangeIdx = _strBounds.ranges.count
551558
currentRange = Range(uncheckedBounds: (endIndex._stringIndex!, endIndex._stringIndex!))
552559
} else {
553-
currentRangeIdx = _strBounds.rangeIdx(containing: i._value)
554-
currentRange = _strBounds.ranges[currentRangeIdx]
560+
(currentRange, currentRangeIdx) = _strBounds.range(containing: i._value)
555561
}
556562
var currentStringIdx = i._value
557563
if currentRange.lowerBound == i._value {
@@ -585,15 +591,13 @@ extension AttributedString.Runs {
585591
attributeNames: [String],
586592
constraints: Set<AttributeRunBoundaries?>
587593
) -> (index: AttributedString.Index, runIndex: AttributedString._InternalRuns.Index) {
588-
precondition(
589-
_strBounds.contains(i._value) || i._value == endIndex._stringIndex,
590-
"AttributedString index is out of bounds")
594+
// _strBounds.range(containing:) below validates that i._value is within the bounds of this slice
591595
precondition(!attributeNames.isEmpty)
592596
let r = _guts.findRun(at: i._value)
593597
if r.runIndex.offset == endIndex._runOffset {
594598
return (i, r.runIndex)
595599
}
596-
let currentRange = _strBounds.ranges[_strBounds.rangeIdx(containing: i._value)]
600+
let currentRange = _strBounds.range(containing: i._value).range
597601

598602
guard constraints.count != 1 || constraints.contains(nil) else {
599603
let nextIndex = _guts.string.unicodeScalars.index(after: i._value)
@@ -719,20 +723,20 @@ extension BigSubstring.UnicodeScalarView {
719723
}
720724

721725
extension RangeSet {
722-
fileprivate func rangeIdx(containing index: Bound) -> Int {
726+
fileprivate func range(containing index: Bound) -> (range: Range<Bound>, offset: Int) {
723727
var start = 0
724728
var end = self.ranges.count
725729
while start < end {
726730
let middle = (start + end) / 2
727731
let value = self.ranges[middle]
728732
if value.contains(index) {
729-
return middle
733+
return (value, middle)
730734
} else if index < value.lowerBound {
731735
end = middle
732736
} else {
733737
start = middle + 1
734738
}
735739
}
736-
preconditionFailure("Internal Inconsistency: Provided index \(index) is out of bounds")
740+
preconditionFailure("AttributedString.Runs index is out of bounds")
737741
}
738742
}

Sources/FoundationEssentials/AttributedString/AttributedStringAttributeStorage.swift

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
1414
extension AttributedString {
1515
internal struct _AttributeValue : Hashable, CustomStringConvertible, Sendable {
16-
typealias RawValue = any Sendable & Hashable
17-
let rawValue: RawValue
16+
private typealias RawValue = any Sendable & Hashable
17+
private let _rawValue: RawValue
1818

1919
// FIXME: If these are always tied to keys, then why are we caching these
2020
// FIXME: on each individual value? Move them to a separate
@@ -24,21 +24,14 @@ extension AttributedString {
2424
let inheritedByAddedText: Bool
2525
let invalidationConditions: Set<AttributeInvalidationCondition>?
2626

27-
var description: String { String(describing: rawValue) }
27+
var description: String { String(describing: _rawValue) }
2828

2929
init<K: AttributedStringKey>(_ value: K.Value, for key: K.Type) where K.Value : Sendable {
30-
rawValue = value
30+
_rawValue = value
3131
runBoundaries = K.runBoundaries
3232
inheritedByAddedText = K.inheritedByAddedText
3333
invalidationConditions = K.invalidationConditions
3434
}
35-
36-
private init<K: AttributedStringKey>(checkingValue value: RawValue, for key: K.Type) where K.Value : Sendable {
37-
guard let trueValue = value as? K.Value else {
38-
fatalError("\(#function) called with non-matching attribute value type")
39-
}
40-
self.init(trueValue, for: K.self)
41-
}
4235

4336
var isInvalidatedOnTextChange: Bool {
4437
invalidationConditions?.contains(.textChanged) ?? false
@@ -61,35 +54,27 @@ extension AttributedString {
6154
func rawValue<K: AttributedStringKey>(
6255
as key: K.Type
6356
) -> K.Value where K.Value: Sendable {
64-
rawValue as! K.Value
57+
func extractValue<RealValue>(_ value: RealValue) -> K.Value {
58+
assert(RealValue.self == K.Value.self, "_AttributeValue raw value can only be retrieved with a key whose value matches the stored attribute value (stored type \(RealValue.self) does not match key value type \(K.Value.self))")
59+
return _identityCast(value, to: K.Value.self)
60+
}
61+
return _openExistential(self._rawValue, do: extractValue)
6562
}
6663

67-
static func ==(lhs: Self, rhs: Self) -> Bool {
68-
Self.__equalAttributes(lhs.rawValue, rhs.rawValue)
64+
static func ==(left: Self, right: Self) -> Bool {
65+
func openEquatableLHS<LeftValue: Hashable & Sendable>(_ leftValue: LeftValue) -> Bool {
66+
func openEquatableRHS<RightValue: Hashable & Sendable>(_ rightValue: RightValue) -> Bool {
67+
assert(LeftValue.self == RightValue.self, "Two _AttributeValues can only be compared if they are of the same attribute value type")
68+
let rightValueAsLeft = _identityCast(rightValue, to: LeftValue.self)
69+
return rightValueAsLeft == leftValue
70+
}
71+
return openEquatableRHS(right._rawValue)
72+
}
73+
return openEquatableLHS(left._rawValue)
6974
}
7075

7176
func hash(into hasher: inout Hasher) {
72-
rawValue.hash(into: &hasher)
73-
}
74-
75-
private static func __equalAttributes(_ lhs: RawValue?, _ rhs: RawValue?) -> Bool {
76-
switch (lhs, rhs) {
77-
case (.none, .none):
78-
return true
79-
case (.none, .some(_)):
80-
return false
81-
case (.some(_), .none):
82-
return false
83-
case (.some(let lhs), .some(let rhs)):
84-
func openEquatable<LHS: Equatable>(_ equatableLHS: LHS) -> Bool {
85-
if let equatableRHS = rhs as? LHS {
86-
return equatableLHS == equatableRHS
87-
} else {
88-
return false
89-
}
90-
}
91-
return openEquatable(lhs)
92-
}
77+
_rawValue.hash(into: &hasher)
9378
}
9479
}
9580
}

0 commit comments

Comments
 (0)