Skip to content

Commit bc78f60

Browse files
authored
Add LambdaRequestID (#243)
If we want to minimize allocations for every invocation, we need to look at types that currently allocate. Currently we use a String to hold the request id. However since the request id is a uuid, that string is 36 characters long. This is way above the 15 character string allocation threshold. The go to type in this case would be `UUID`. However `UUID` is in Foundation and we want to keep the lambda runtime Foundation free. This pr introduces a LambdaRequestID to represent a uuid. One nice side effect of having our own uuid case is: We can make writing the uuid-string to a ByteBuffer allocation free (since no intermediate translation to a string is needed first).
1 parent b8d89ca commit bc78f60

File tree

3 files changed

+615
-0
lines changed

3 files changed

+615
-0
lines changed

NOTICE.txt

+9
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,12 @@ This product contains a derivation various scripts from SwiftNIO.
3333
* https://www.apache.org/licenses/LICENSE-2.0
3434
* HOMEPAGE:
3535
* https://github.com/apple/swift-nio
36+
37+
---
38+
39+
This product contains a derivation of the swift-extras' 'swift-extras-uuid'.
40+
41+
* LICENSE (MIT):
42+
* https://github.com/swift-extras/swift-extras-uuid/blob/main/LICENSE
43+
* HOMEPAGE:
44+
* https://github.com/swift-extras/swift-extras-uuid
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
17+
// This is heavily inspired by:
18+
// https://github.com/swift-extras/swift-extras-uuid
19+
20+
struct LambdaRequestID {
21+
typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
22+
23+
var uuid: uuid_t {
24+
self._uuid
25+
}
26+
27+
static let null: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
28+
29+
/// Creates a random [v4](https://tools.ietf.org/html/rfc4122#section-4.1.3) UUID.
30+
init() {
31+
self = Self.generateRandom()
32+
}
33+
34+
init?(uuidString: String) {
35+
guard uuidString.utf8.count == 36 else {
36+
return nil
37+
}
38+
39+
if let requestID = uuidString.utf8.withContiguousStorageIfAvailable({ ptr -> LambdaRequestID? in
40+
let rawBufferPointer = UnsafeRawBufferPointer(ptr)
41+
let requestID = Self.fromPointer(rawBufferPointer)
42+
return requestID
43+
}) {
44+
if let requestID = requestID {
45+
self = requestID
46+
} else {
47+
return nil
48+
}
49+
} else {
50+
var newSwiftCopy = uuidString
51+
newSwiftCopy.makeContiguousUTF8()
52+
if let value = Self(uuidString: newSwiftCopy) {
53+
self = value
54+
} else {
55+
return nil
56+
}
57+
}
58+
}
59+
60+
/// Creates a UUID from a `uuid_t`.
61+
init(uuid: uuid_t) {
62+
self._uuid = uuid
63+
}
64+
65+
private let _uuid: uuid_t
66+
67+
/// Returns a string representation for the `LambdaRequestID`, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
68+
var uuidString: String {
69+
self.uppercased
70+
}
71+
72+
/// Returns a lowercase string representation for the `LambdaRequestID`, such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f"
73+
var lowercased: String {
74+
var bytes = self.toAsciiBytesOnStack(characters: Self.lowercaseLookup)
75+
return withUnsafeBytes(of: &bytes) {
76+
String(decoding: $0, as: Unicode.UTF8.self)
77+
}
78+
}
79+
80+
/// Returns an uppercase string representation for the `LambdaRequestID`, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
81+
var uppercased: String {
82+
var bytes = self.toAsciiBytesOnStack(characters: Self.uppercaseLookup)
83+
return withUnsafeBytes(of: &bytes) {
84+
String(decoding: $0, as: Unicode.UTF8.self)
85+
}
86+
}
87+
88+
/// thread safe secure random number generator.
89+
private static var generator = SystemRandomNumberGenerator()
90+
private static func generateRandom() -> Self {
91+
var _uuid: uuid_t = LambdaRequestID.null
92+
// https://tools.ietf.org/html/rfc4122#page-14
93+
// o Set all the other bits to randomly (or pseudo-randomly) chosen
94+
// values.
95+
withUnsafeMutableBytes(of: &_uuid) { ptr in
96+
ptr.storeBytes(of: Self.generator.next(), toByteOffset: 0, as: UInt64.self)
97+
ptr.storeBytes(of: Self.generator.next(), toByteOffset: 8, as: UInt64.self)
98+
}
99+
100+
// o Set the four most significant bits (bits 12 through 15) of the
101+
// time_hi_and_version field to the 4-bit version number from
102+
// Section 4.1.3.
103+
_uuid.6 = (_uuid.6 & 0x0F) | 0x40
104+
105+
// o Set the two most significant bits (bits 6 and 7) of the
106+
// clock_seq_hi_and_reserved to zero and one, respectively.
107+
_uuid.8 = (_uuid.8 & 0x3F) | 0x80
108+
return LambdaRequestID(uuid: _uuid)
109+
}
110+
}
111+
112+
// MARK: - Protocol extensions -
113+
114+
extension LambdaRequestID: Equatable {
115+
// sadly no auto conformance from the compiler
116+
static func == (lhs: Self, rhs: Self) -> Bool {
117+
lhs._uuid.0 == rhs._uuid.0 &&
118+
lhs._uuid.1 == rhs._uuid.1 &&
119+
lhs._uuid.2 == rhs._uuid.2 &&
120+
lhs._uuid.3 == rhs._uuid.3 &&
121+
lhs._uuid.4 == rhs._uuid.4 &&
122+
lhs._uuid.5 == rhs._uuid.5 &&
123+
lhs._uuid.6 == rhs._uuid.6 &&
124+
lhs._uuid.7 == rhs._uuid.7 &&
125+
lhs._uuid.8 == rhs._uuid.8 &&
126+
lhs._uuid.9 == rhs._uuid.9 &&
127+
lhs._uuid.10 == rhs._uuid.10 &&
128+
lhs._uuid.11 == rhs._uuid.11 &&
129+
lhs._uuid.12 == rhs._uuid.12 &&
130+
lhs._uuid.13 == rhs._uuid.13 &&
131+
lhs._uuid.14 == rhs._uuid.14 &&
132+
lhs._uuid.15 == rhs._uuid.15
133+
}
134+
}
135+
136+
extension LambdaRequestID: Hashable {
137+
func hash(into hasher: inout Hasher) {
138+
var value = self._uuid
139+
withUnsafeBytes(of: &value) { ptr in
140+
hasher.combine(bytes: ptr)
141+
}
142+
}
143+
}
144+
145+
extension LambdaRequestID: CustomStringConvertible {
146+
var description: String {
147+
self.uuidString
148+
}
149+
}
150+
151+
extension LambdaRequestID: CustomDebugStringConvertible {
152+
var debugDescription: String {
153+
self.uuidString
154+
}
155+
}
156+
157+
extension LambdaRequestID: Decodable {
158+
init(from decoder: Decoder) throws {
159+
let container = try decoder.singleValueContainer()
160+
let uuidString = try container.decode(String.self)
161+
162+
guard let uuid = LambdaRequestID.fromString(uuidString) else {
163+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Attempted to decode UUID from invalid UUID string.")
164+
}
165+
166+
self = uuid
167+
}
168+
}
169+
170+
extension LambdaRequestID: Encodable {
171+
func encode(to encoder: Encoder) throws {
172+
var container = encoder.singleValueContainer()
173+
try container.encode(self.uuidString)
174+
}
175+
}
176+
177+
// MARK: - Implementation details -
178+
179+
extension LambdaRequestID {
180+
fileprivate static let lowercaseLookup: [UInt8] = [
181+
UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"),
182+
UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"),
183+
UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "a"), UInt8(ascii: "b"),
184+
UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"),
185+
]
186+
187+
fileprivate static let uppercaseLookup: [UInt8] = [
188+
UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"),
189+
UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"),
190+
UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "A"), UInt8(ascii: "B"),
191+
UInt8(ascii: "C"), UInt8(ascii: "D"), UInt8(ascii: "E"), UInt8(ascii: "F"),
192+
]
193+
194+
/// Use this type to create a backing store for a 8-4-4-4-12 UUID String on stack.
195+
///
196+
/// Using this type we ensure to only have one allocation for creating a String even before Swift 5.3 and it can
197+
/// also be used as an intermediary before copying the string bytes into a NIO `ByteBuffer`.
198+
fileprivate typealias uuid_string_t = (
199+
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
200+
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
201+
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
202+
)
203+
204+
fileprivate static let nullString: uuid_string_t = (
205+
0, 0, 0, 0, 0, 0, 0, 0, UInt8(ascii: "-"),
206+
0, 0, 0, 0, UInt8(ascii: "-"),
207+
0, 0, 0, 0, UInt8(ascii: "-"),
208+
0, 0, 0, 0, UInt8(ascii: "-"),
209+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
210+
)
211+
212+
fileprivate func toAsciiBytesOnStack(characters: [UInt8]) -> uuid_string_t {
213+
var string: uuid_string_t = Self.nullString
214+
// to get the best performance we access the lookup table's unsafe buffer pointer
215+
// since the lookup table has 16 elements and we shift the byte values in such a way
216+
// that the max value is 15 (last 4 bytes = 16 values). For this reason the lookups
217+
// are safe and we don't need Swifts safety guards.
218+
219+
characters.withUnsafeBufferPointer { lookup in
220+
string.0 = lookup[Int(uuid.0 >> 4)]
221+
string.1 = lookup[Int(uuid.0 & 0x0F)]
222+
string.2 = lookup[Int(uuid.1 >> 4)]
223+
string.3 = lookup[Int(uuid.1 & 0x0F)]
224+
string.4 = lookup[Int(uuid.2 >> 4)]
225+
string.5 = lookup[Int(uuid.2 & 0x0F)]
226+
string.6 = lookup[Int(uuid.3 >> 4)]
227+
string.7 = lookup[Int(uuid.3 & 0x0F)]
228+
string.9 = lookup[Int(uuid.4 >> 4)]
229+
string.10 = lookup[Int(uuid.4 & 0x0F)]
230+
string.11 = lookup[Int(uuid.5 >> 4)]
231+
string.12 = lookup[Int(uuid.5 & 0x0F)]
232+
string.14 = lookup[Int(uuid.6 >> 4)]
233+
string.15 = lookup[Int(uuid.6 & 0x0F)]
234+
string.16 = lookup[Int(uuid.7 >> 4)]
235+
string.17 = lookup[Int(uuid.7 & 0x0F)]
236+
string.19 = lookup[Int(uuid.8 >> 4)]
237+
string.20 = lookup[Int(uuid.8 & 0x0F)]
238+
string.21 = lookup[Int(uuid.9 >> 4)]
239+
string.22 = lookup[Int(uuid.9 & 0x0F)]
240+
string.24 = lookup[Int(uuid.10 >> 4)]
241+
string.25 = lookup[Int(uuid.10 & 0x0F)]
242+
string.26 = lookup[Int(uuid.11 >> 4)]
243+
string.27 = lookup[Int(uuid.11 & 0x0F)]
244+
string.28 = lookup[Int(uuid.12 >> 4)]
245+
string.29 = lookup[Int(uuid.12 & 0x0F)]
246+
string.30 = lookup[Int(uuid.13 >> 4)]
247+
string.31 = lookup[Int(uuid.13 & 0x0F)]
248+
string.32 = lookup[Int(uuid.14 >> 4)]
249+
string.33 = lookup[Int(uuid.14 & 0x0F)]
250+
string.34 = lookup[Int(uuid.15 >> 4)]
251+
string.35 = lookup[Int(uuid.15 & 0x0F)]
252+
}
253+
254+
return string
255+
}
256+
257+
static func fromString(_ string: String) -> LambdaRequestID? {
258+
guard string.utf8.count == 36 else {
259+
// invalid length
260+
return nil
261+
}
262+
var string = string
263+
return string.withUTF8 {
264+
LambdaRequestID.fromPointer(.init($0))
265+
}
266+
}
267+
}
268+
269+
extension LambdaRequestID {
270+
static func fromPointer(_ ptr: UnsafeRawBufferPointer) -> LambdaRequestID? {
271+
func uint4Value(from value: UInt8, valid: inout Bool) -> UInt8 {
272+
switch value {
273+
case UInt8(ascii: "0") ... UInt8(ascii: "9"):
274+
return value &- UInt8(ascii: "0")
275+
case UInt8(ascii: "a") ... UInt8(ascii: "f"):
276+
return value &- UInt8(ascii: "a") &+ 10
277+
case UInt8(ascii: "A") ... UInt8(ascii: "F"):
278+
return value &- UInt8(ascii: "A") &+ 10
279+
default:
280+
valid = false
281+
return 0
282+
}
283+
}
284+
285+
func dashCheck(from value: UInt8, valid: inout Bool) {
286+
if value != UInt8(ascii: "-") {
287+
valid = false
288+
}
289+
}
290+
291+
precondition(ptr.count == 36)
292+
var uuid = Self.null
293+
var valid = true
294+
uuid.0 = uint4Value(from: ptr[0], valid: &valid) &<< 4 &+ uint4Value(from: ptr[1], valid: &valid)
295+
uuid.1 = uint4Value(from: ptr[2], valid: &valid) &<< 4 &+ uint4Value(from: ptr[3], valid: &valid)
296+
uuid.2 = uint4Value(from: ptr[4], valid: &valid) &<< 4 &+ uint4Value(from: ptr[5], valid: &valid)
297+
uuid.3 = uint4Value(from: ptr[6], valid: &valid) &<< 4 &+ uint4Value(from: ptr[7], valid: &valid)
298+
dashCheck(from: ptr[8], valid: &valid)
299+
uuid.4 = uint4Value(from: ptr[9], valid: &valid) &<< 4 &+ uint4Value(from: ptr[10], valid: &valid)
300+
uuid.5 = uint4Value(from: ptr[11], valid: &valid) &<< 4 &+ uint4Value(from: ptr[12], valid: &valid)
301+
dashCheck(from: ptr[13], valid: &valid)
302+
uuid.6 = uint4Value(from: ptr[14], valid: &valid) &<< 4 &+ uint4Value(from: ptr[15], valid: &valid)
303+
uuid.7 = uint4Value(from: ptr[16], valid: &valid) &<< 4 &+ uint4Value(from: ptr[17], valid: &valid)
304+
dashCheck(from: ptr[18], valid: &valid)
305+
uuid.8 = uint4Value(from: ptr[19], valid: &valid) &<< 4 &+ uint4Value(from: ptr[20], valid: &valid)
306+
uuid.9 = uint4Value(from: ptr[21], valid: &valid) &<< 4 &+ uint4Value(from: ptr[22], valid: &valid)
307+
dashCheck(from: ptr[23], valid: &valid)
308+
uuid.10 = uint4Value(from: ptr[24], valid: &valid) &<< 4 &+ uint4Value(from: ptr[25], valid: &valid)
309+
uuid.11 = uint4Value(from: ptr[26], valid: &valid) &<< 4 &+ uint4Value(from: ptr[27], valid: &valid)
310+
uuid.12 = uint4Value(from: ptr[28], valid: &valid) &<< 4 &+ uint4Value(from: ptr[29], valid: &valid)
311+
uuid.13 = uint4Value(from: ptr[30], valid: &valid) &<< 4 &+ uint4Value(from: ptr[31], valid: &valid)
312+
uuid.14 = uint4Value(from: ptr[32], valid: &valid) &<< 4 &+ uint4Value(from: ptr[33], valid: &valid)
313+
uuid.15 = uint4Value(from: ptr[34], valid: &valid) &<< 4 &+ uint4Value(from: ptr[35], valid: &valid)
314+
315+
if valid {
316+
return LambdaRequestID(uuid: uuid)
317+
}
318+
319+
return nil
320+
}
321+
}
322+
323+
extension ByteBuffer {
324+
func getRequestID(at index: Int) -> LambdaRequestID? {
325+
guard let range = self.rangeWithinReadableBytes(index: index, length: 36) else {
326+
return nil
327+
}
328+
return self.withUnsafeReadableBytes { ptr in
329+
LambdaRequestID.fromPointer(UnsafeRawBufferPointer(fastRebase: ptr[range]))
330+
}
331+
}
332+
333+
mutating func readRequestID() -> LambdaRequestID? {
334+
guard let requestID = self.getRequestID(at: self.readerIndex) else {
335+
return nil
336+
}
337+
self.moveReaderIndex(forwardBy: 36)
338+
return requestID
339+
}
340+
341+
@discardableResult
342+
mutating func setRequestID(_ requestID: LambdaRequestID, at index: Int) -> Int {
343+
var localBytes = requestID.toAsciiBytesOnStack(characters: LambdaRequestID.lowercaseLookup)
344+
return withUnsafeBytes(of: &localBytes) {
345+
self.setBytes($0, at: index)
346+
}
347+
}
348+
349+
mutating func writeRequestID(_ requestID: LambdaRequestID) -> Int {
350+
let length = self.setRequestID(requestID, at: self.writerIndex)
351+
self.moveWriterIndex(forwardBy: length)
352+
return length
353+
}
354+
355+
// copy and pasted from NIOCore
356+
func rangeWithinReadableBytes(index: Int, length: Int) -> Range<Int>? {
357+
guard index >= self.readerIndex && length >= 0 else {
358+
return nil
359+
}
360+
361+
// both these &-s are safe, they can't underflow because both left & right side are >= 0 (and index >= readerIndex)
362+
let indexFromReaderIndex = index &- self.readerIndex
363+
assert(indexFromReaderIndex >= 0)
364+
guard indexFromReaderIndex <= self.readableBytes &- length else {
365+
return nil
366+
}
367+
368+
let upperBound = indexFromReaderIndex &+ length // safe, can't overflow, we checked it above.
369+
370+
// uncheckedBounds is safe because `length` is >= 0, so the lower bound will always be lower/equal to upper
371+
return Range<Int>(uncheckedBounds: (lower: indexFromReaderIndex, upper: upperBound))
372+
}
373+
}
374+
375+
// copy and pasted from NIOCore
376+
extension UnsafeRawBufferPointer {
377+
init(fastRebase slice: Slice<UnsafeRawBufferPointer>) {
378+
let base = slice.base.baseAddress?.advanced(by: slice.startIndex)
379+
self.init(start: base, count: slice.endIndex &- slice.startIndex)
380+
}
381+
}

0 commit comments

Comments
 (0)