Skip to content

Commit f41ab27

Browse files
authored
Make PeerAddress/init faster (#37)
`PeerAddress/init` currently uses `String` and `split`s them, which is not very optimal. This PR uses UTF8View instead to avoid allocations/make things faster
1 parent c32c03d commit f41ab27

File tree

2 files changed

+89
-23
lines changed

2 files changed

+89
-23
lines changed

Sources/GRPCOTelTracingInterceptors/Tracing/SpanAttributes+GRPCTracingKeys.swift

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -136,47 +136,91 @@ package enum PeerAddress: Equatable {
136136
// - ipv4:<host>:<port> for ipv4 addresses
137137
// - ipv6:[<host>]:<port> for ipv6 addresses
138138
// - unix:<uds-pathname> for UNIX domain sockets
139+
let addressUTF8View = address.utf8
139140

140141
// First get the first component so that we know what type of address we're dealing with
141-
let addressComponents = address.split(separator: ":", maxSplits: 1)
142+
let firstColonIndex = addressUTF8View.firstIndex(of: UInt8(ascii: ":"))
142143

143-
guard addressComponents.count > 1 else {
144+
guard let firstColonIndex else {
144145
// This is some unexpected/unknown format
145146
return nil
146147
}
147148

149+
let addressType = addressUTF8View[..<firstColonIndex]
150+
151+
var addressWithoutType = addressUTF8View[firstColonIndex...]
152+
addressWithoutType.removeFirst()
153+
148154
// Check what type the transport is...
149-
switch addressComponents[0] {
150-
case "ipv4":
151-
let ipv4AddressComponents = addressComponents[1].split(separator: ":")
152-
if ipv4AddressComponents.count == 2, let port = Int(ipv4AddressComponents[1]) {
153-
self = .ipv4(address: String(ipv4AddressComponents[0]), port: port)
154-
} else {
155+
if addressType.elementsEqual("ipv4".utf8) {
156+
guard let addressColon = addressWithoutType.firstIndex(of: UInt8(ascii: ":")) else {
157+
// This is some unexpected/unknown format
155158
return nil
156159
}
157160

158-
case "ipv6":
159-
if addressComponents[1].first == "[" {
160-
// At this point, we are looking at an address with format: [<address>]:<port>
161-
// We drop the first character ('[') and split by ']:' to keep two components: the address
162-
// and the port.
163-
let ipv6AddressComponents = addressComponents[1].dropFirst().split(separator: "]:")
164-
if ipv6AddressComponents.count == 2, let port = Int(ipv6AddressComponents[1]) {
165-
self = .ipv6(address: String(ipv6AddressComponents[0]), port: port)
166-
} else {
167-
return nil
168-
}
161+
let hostComponent = addressWithoutType[..<addressColon]
162+
var portComponent = addressWithoutType[addressColon...]
163+
portComponent.removeFirst()
164+
165+
if let host = String(hostComponent), let port = Int(ipAddressPortStringBytes: portComponent) {
166+
self = .ipv4(address: host, port: port)
169167
} else {
170168
return nil
171169
}
170+
} else if addressType.elementsEqual("ipv6".utf8) {
171+
guard let lastColonIndex = addressWithoutType.lastIndex(of: UInt8(ascii: ":")) else {
172+
// This is some unexpected/unknown format
173+
return nil
174+
}
172175

173-
case "unix":
174-
// Whatever comes after "unix:" is the <pathname>
175-
self = .unixDomainSocket(path: String(addressComponents[1]))
176+
var hostComponent = addressWithoutType[..<lastColonIndex]
177+
var portComponent = addressWithoutType[lastColonIndex...]
178+
portComponent.removeFirst()
176179

177-
default:
180+
if let firstBracket = hostComponent.popFirst(), let lastBracket = hostComponent.popLast(),
181+
firstBracket == UInt8(ascii: "["), lastBracket == UInt8(ascii: "]"),
182+
let host = String(hostComponent), let port = Int(ipAddressPortStringBytes: portComponent)
183+
{
184+
self = .ipv6(address: host, port: port)
185+
} else {
186+
// This is some unexpected/unknown format
187+
return nil
188+
}
189+
} else if addressType.elementsEqual("unix".utf8) {
190+
// Whatever comes after "unix:" is the <pathname>
191+
self = .unixDomainSocket(path: String(addressWithoutType) ?? "")
192+
} else {
178193
// This is some unexpected/unknown format
179194
return nil
180195
}
181196
}
182197
}
198+
199+
extension Int {
200+
package init?(ipAddressPortStringBytes: some Collection<UInt8>) {
201+
guard (1 ... 5).contains(ipAddressPortStringBytes.count) else {
202+
// Valid IP port values go up to 2^16-1 (65535), which is 5 digits long.
203+
// If the string we get is over 5 characters, we know for sure that this is an invalid port.
204+
// If it's empty, we also know it's invalid as we need at least one digit.
205+
return nil
206+
}
207+
208+
var value = 0
209+
for utf8Char in ipAddressPortStringBytes {
210+
value &*= 10
211+
guard (UInt8(ascii: "0") ... UInt8(ascii: "9")).contains(utf8Char) else {
212+
// non-digit character
213+
return nil
214+
}
215+
value &+= Int(utf8Char - UInt8(ascii: "0"))
216+
}
217+
218+
guard value <= Int(UInt16.max) else {
219+
// Valid IP port values go up to 2^16-1.
220+
// If a number greater than this was given, it can't be a valid port.
221+
return nil
222+
}
223+
224+
self = value
225+
}
226+
}

Tests/GRPCOTelTracingInterceptorsTests/PeerAddressTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,26 @@ struct PeerAddressTests {
5656
let address = PeerAddress(address)
5757
#expect(address == nil)
5858
}
59+
60+
@Test(
61+
"Int.init(utf8View:)",
62+
arguments: [
63+
("1", 1),
64+
("21", 21),
65+
("321", 321),
66+
("4321", 4321),
67+
("54321", 54321),
68+
("65536", nil), // Invalid: over 65535 IP port limit
69+
("654321", nil), // Invalid: over 5 digits
70+
("abc", nil), // Invalid: no digits
71+
("a123", nil), // Invalid: mixed digits and chars outside the valid ascii range for digits
72+
("123a", nil), // Invalid: mixed digits and chars outside the valid ascii range for digits
73+
("(123", nil), // Invalid: mixed digits and chars outside the valid ascii range for digits
74+
("123(", nil), // Invalid: mixed digits and chars outside the valid ascii range for digits
75+
("", nil), // Invalid: empty string
76+
]
77+
)
78+
func testIntInitFromUTF8View(string: String, expectedInt: Int?) async throws {
79+
#expect(expectedInt == Int(ipAddressPortStringBytes: string.utf8))
80+
}
5981
}

0 commit comments

Comments
 (0)