From 38d42521e284fbab54e53d45a7e269a4b755e4be Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 8 Nov 2024 01:16:21 -0600 Subject: [PATCH] QOL stuff, and... - added some documentation - added `trailingNonzeroByteCount` and `trailingZeroByteCount` computed properties to `SIMD` - now working on using SIMD to parse http requests --- Sources/Destiny/Server.swift | 3 +- Sources/Destiny/Socket.swift | 74 +++++++++-- .../DestinyUtilities/DynamicResponses.swift | 5 +- Sources/DestinyUtilities/DynamicRoute.swift | 2 +- Sources/DestinyUtilities/PathComponent.swift | 30 ++--- Sources/DestinyUtilities/SocketProtocol.swift | 4 +- Sources/DestinyUtilities/StackStrings.swift | 124 ++++++++++++++++++ Sources/DestinyUtilities/Utilities.swift | 3 + Tests/DestinyTests/DestinyTests.swift | 78 ++++++----- 9 files changed, 259 insertions(+), 64 deletions(-) diff --git a/Sources/Destiny/Server.swift b/Sources/Destiny/Server.swift index 4e33b03..b23e178 100644 --- a/Sources/Destiny/Server.swift +++ b/Sources/Destiny/Server.swift @@ -147,6 +147,7 @@ enum ClientProcessing { shutdown(client, 2) // shutdown read and write (https://www.gnu.org/software/libc/manual/html_node/Closing-a-Socket.html) close(client) } + //let request:Request = try client_socket.loadRequest() let token:DestinyRoutePathType = try client_socket.readLineSIMD() if let responder:StaticRouteResponseProtocol = static_responses[token] { if responder.isAsync { @@ -156,7 +157,7 @@ enum ClientProcessing { } } else if let (start_line, route):(HTTPStartLine, DynamicRouteResponseProtocol) = dynamic_responses[token] { let headers:[String:String] = try client_socket.readHeaders() - let request:Request = Request(method: start_line.method, path: start_line.path, version: start_line.version, headers: headers, body: "") + let request:Request = Request(token: token, method: start_line.method, path: start_line.path, version: start_line.version, headers: headers, body: "") var response:DynamicResponseProtocol = route.defaultResponse for index in route.parameterPathIndexes { diff --git a/Sources/Destiny/Socket.swift b/Sources/Destiny/Socket.swift index 1eefe58..d27e796 100644 --- a/Sources/Destiny/Socket.swift +++ b/Sources/Destiny/Socket.swift @@ -60,19 +60,45 @@ public extension Socket { return line } + /// Reads `scalarCount` characters and loads them into the target SIMD. @inlinable - func readLineSIMD() throws -> T where T.Scalar: BinaryInteger { // read just the method, path & http version + func readLineSIMD2() throws -> (T, Int) where T.Scalar == UInt8 { // read just the method, path & http version var string:T = T() - var i:Int = 0, char:UInt8 = 0 + let read:Int = try withUnsafeMutableBytes(of: &string) { p in + return try readSIMDBuffer(into: p.baseAddress!, length: T.scalarCount) + } + return (string, read) + } + + func loadRequest() throws -> Request { + var test:[SIMD64] = [] + test.reserveCapacity(10) // maximum of 640 bytes; decent starting point + var head_count:Int = 0 + var token:DestinyRoutePathType = .init() while true { - char = try readByte() - if char == 10 || i == T.scalarCount { + let (line, read):(SIMD64, Int) = try readLineSIMD2() + if head_count == 0 { + token = line.lowHalf + } + if read == 0 { break - } else if char == 13 { - continue } - string[i] = T.Scalar(char) - i += 1 + test.append(line) + head_count += read + if read < 64 { + break + } + } + print("test strings=\(test.map({ $0.string() }))") + return Request(token: token, method: .get, path: ["dynamic", "rekt"], version: "HTTP/1.1", headers: [:], body: "") + } + + /// Reads `scalarCount` characters and loads them into the target SIMD. + @inlinable + func readLineSIMD() throws -> T where T.Scalar == UInt8 { // read just the method, path & http version + var string:T = T() + let _:Int = try withUnsafeMutableBytes(of: &string) { p in + return try readBuffer(into: p.baseAddress!, length: T.scalarCount) } return string } @@ -95,6 +121,7 @@ public extension Socket { guard let baseAddress:UnsafeMutablePointer = buffer.baseAddress else { return 0 } return try readBuffer(into: baseAddress, length: length) } + /// Reads multiple bytes and writes them into a buffer @inlinable func readBuffer(into baseAddress: UnsafeMutablePointer, length: Int) throws -> Int { @@ -112,6 +139,37 @@ public extension Socket { } return bytes_read } + /// Reads multiple bytes and writes them into a buffer + @inlinable + func readBuffer(into baseAddress: UnsafeMutableRawPointer, length: Int) throws -> Int { + var bytes_read:Int = 0 + while bytes_read < length { + if Task.isCancelled { return 0 } + let to_read:Int = min(Self.bufferLength, length - bytes_read) + let read_bytes:Int = recv(fileDescriptor, baseAddress + bytes_read, to_read, 0) + if read_bytes < 0 { // error + throw SocketError.readBufferFailed() + } else if read_bytes == 0 { // end of file + break + } + bytes_read += read_bytes + } + return bytes_read + } + + /// Reads multiple bytes and writes them into a buffer + @inlinable + func readSIMDBuffer(into baseAddress: UnsafeMutableRawPointer, length: Int) throws -> Int { + var bytes_read:Int = 0 + if Task.isCancelled { return 0 } + let to_read:Int = min(Self.bufferLength, length - bytes_read) + let read_bytes:Int = recv(fileDescriptor, baseAddress + bytes_read, to_read, 0) + if read_bytes < 0 { // error + throw SocketError.readBufferFailed() + } + bytes_read += read_bytes + return bytes_read + } } // MARK: Socket writing diff --git a/Sources/DestinyUtilities/DynamicResponses.swift b/Sources/DestinyUtilities/DynamicResponses.swift index 20e4670..d668750 100644 --- a/Sources/DestinyUtilities/DynamicResponses.swift +++ b/Sources/DestinyUtilities/DynamicResponses.swift @@ -40,8 +40,9 @@ public struct DynamicResponses : Sendable { return nil } let values:[String] = spaced[1].splitSIMD(separator: 47).map({ $0.string() }) // 1 = the target route path; 47 = slash + let start_line:HTTPStartLine = HTTPStartLine(method: method, path: values, version: version) if let responder:DynamicRouteResponseProtocol = parameterless[token] { - return (HTTPStartLine(method: method, path: values, version: version), responder) + return (start_line, responder) } guard let routes:[DynamicRouteResponseProtocol] = parameterized.get(values.count) else { return nil } for route in routes { @@ -54,7 +55,7 @@ public struct DynamicResponses : Sendable { } } if found { - return (HTTPStartLine(method: method, path: values, version: version), route) + return (start_line, route) } } return nil diff --git a/Sources/DestinyUtilities/DynamicRoute.swift b/Sources/DestinyUtilities/DynamicRoute.swift index 07d501b..1231697 100644 --- a/Sources/DestinyUtilities/DynamicRoute.swift +++ b/Sources/DestinyUtilities/DynamicRoute.swift @@ -82,7 +82,7 @@ public extension DynamicRoute { method_string = argument.expression.memberAccess!.declName.baseName.text.uppercased() break case "path": - path = argument.expression.array!.elements.map({ PathComponent.parse($0.expression) }) + path = argument.expression.array!.elements.map({ PathComponent(expression: $0.expression) }) for component in path.filter({ $0.isParameter }) { parameters[component.value] = "" } diff --git a/Sources/DestinyUtilities/PathComponent.swift b/Sources/DestinyUtilities/PathComponent.swift index 19832ed..2bd5c26 100644 --- a/Sources/DestinyUtilities/PathComponent.swift +++ b/Sources/DestinyUtilities/PathComponent.swift @@ -7,6 +7,7 @@ import SwiftSyntax +/// Represents an individual path value for a route. Used to determine how to handle a route responder for dynamic routes with parameters at compile time. public enum PathComponent : Sendable, CustomStringConvertible, ExpressibleByStringLiteral { public typealias StringLiteralType = String public typealias ExtendedGraphemeClusterLiteralType = String @@ -15,16 +16,20 @@ public enum PathComponent : Sendable, CustomStringConvertible, ExpressibleByStri case literal(String) case parameter(String) + public init(expression: ExprSyntax) { + self = .init(stringLiteral: expression.stringLiteral?.string ?? expression.functionCall!.calledExpression.memberAccess!.declName.baseName.text) + } public init(stringLiteral value: String) { if value.first == ":" { - self = .parameter(String(value[value.index(after: value.startIndex)...])) + self = .parameter(value[value.index(after: value.startIndex)...].replacingOccurrences(of: ":", with: "")) } else { - self = .literal(value) + self = .literal(value.replacingOccurrences(of: ":", with: "")) } } public var description : String { "\"" + slug + "\"" } + /// Whether or not this component is a parameter. public var isParameter : Bool { switch self { case .literal(_): return false @@ -32,6 +37,7 @@ public enum PathComponent : Sendable, CustomStringConvertible, ExpressibleByStri } } + /// String representation of this component including the delimiter, if it is a parameter. Used to determine where parameters are located in a route's path at compile time. public var slug : String { switch self { case .literal(let value): return value @@ -39,29 +45,11 @@ public enum PathComponent : Sendable, CustomStringConvertible, ExpressibleByStri } } + /// String representation of this component where the delimiter is omitted (only the name of the path is present). public var value : String { switch self { case .literal(let value): return value case .parameter(let value): return value } } -} - -public extension PathComponent { - static func parse(_ expression: ExprSyntax) -> Self { - if var string:String = expression.stringLiteral?.string { - let is_parameter:Bool = string[string.startIndex] == ":" - string.replace(":", with: "") - return is_parameter ? .parameter(string) : .literal(string) - } else { - let function:FunctionCallExprSyntax = expression.functionCall! - let target:String = function.calledExpression.memberAccess!.declName.baseName.text - let value:String = function.arguments.first!.expression.stringLiteral!.string.replacing(":", with: "") - switch target { - case "literal": return .literal(value) - case "parameter": return .parameter(value) - default: return .literal(value) - } - } - } } \ No newline at end of file diff --git a/Sources/DestinyUtilities/SocketProtocol.swift b/Sources/DestinyUtilities/SocketProtocol.swift index 165c0b2..ba242f3 100644 --- a/Sources/DestinyUtilities/SocketProtocol.swift +++ b/Sources/DestinyUtilities/SocketProtocol.swift @@ -19,7 +19,9 @@ public protocol SocketProtocol : ~Copyable { init(fileDescriptor: Int32) - @inlinable func readLineSIMD() throws -> T where T.Scalar: BinaryInteger + @inlinable func loadRequest() throws -> Request + + @inlinable func readLineSIMD() throws -> T where T.Scalar == UInt8 @inlinable func readHeaders() throws -> [String:String] // TODO: make faster (replace with a SIMD/StackString equivalent) @inlinable func readBuffer(into baseAddress: UnsafeMutablePointer, length: Int) throws -> Int diff --git a/Sources/DestinyUtilities/StackStrings.swift b/Sources/DestinyUtilities/StackStrings.swift index a175789..b86fac0 100644 --- a/Sources/DestinyUtilities/StackStrings.swift +++ b/Sources/DestinyUtilities/StackStrings.swift @@ -402,6 +402,130 @@ public extension SIMD64 where Scalar : BinaryInteger { } } +// MARK: trailingNonzeroByteCount O(1) +// implementation should never change +public extension SIMD2 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingNonzeroByteCount : Int { + if y == 0 { return 0 } + if x == 0 { return 1 } + return scalarCount + } +} +public extension SIMD4 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingNonzeroByteCount : Int { + let all_nonzero:SIMDMask> = .init(repeating: true) + if (highHalf .!= .zero) != all_nonzero { return highHalf.trailingNonzeroByteCount } + if (lowHalf .!= .zero) != all_nonzero { return 2 + lowHalf.trailingNonzeroByteCount } + return scalarCount + } +} +public extension SIMD8 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingNonzeroByteCount : Int { + let all_nonzero:SIMDMask> = .init(repeating: true) + if (highHalf .!= .zero) != all_nonzero { return highHalf.trailingNonzeroByteCount } + if (lowHalf .!= .zero) != all_nonzero { return 4 + lowHalf.trailingNonzeroByteCount } + return scalarCount + } +} +public extension SIMD16 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingNonzeroByteCount : Int { + let all_nonzero:SIMDMask> = .init(repeating: true) + if (highHalf .!= .zero) != all_nonzero { return highHalf.trailingNonzeroByteCount } + if (lowHalf .!= .zero) != all_nonzero { return 8 + lowHalf.trailingNonzeroByteCount } + return scalarCount + } +} +public extension SIMD32 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingNonzeroByteCount : Int { + let all_nonzero:SIMDMask> = .init(repeating: true) + if (highHalf .!= .zero) != all_nonzero { return highHalf.trailingNonzeroByteCount } + if (lowHalf .!= .zero) != all_nonzero { return 16 + lowHalf.trailingNonzeroByteCount } + return scalarCount + } +} +public extension SIMD64 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingNonzeroByteCount : Int { + let all_nonzero:SIMDMask> = .init(repeating: true) + if (highHalf .!= .zero) != all_nonzero { return highHalf.trailingNonzeroByteCount } + if (lowHalf .!= .zero) != all_nonzero { return 32 + lowHalf.trailingNonzeroByteCount } + return scalarCount + } +} + +// MARK: trailingZeroByteCount O(1) +// implementation should never change +public extension SIMD2 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingZeroByteCount : Int { + if y != 0 { return 0 } + if x != 0 { return 1 } + return scalarCount + } +} +public extension SIMD4 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingZeroByteCount : Int { + let all_zero:SIMDMask> = .init(repeating: true) + if (highHalf .== .zero) != all_zero { return highHalf.trailingZeroByteCount } + if (lowHalf .== .zero) != all_zero { return 2 + lowHalf.trailingZeroByteCount } + return scalarCount + } +} +public extension SIMD8 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingZeroByteCount : Int { + let all_zero:SIMDMask> = .init(repeating: true) + if (highHalf .== .zero) != all_zero { return highHalf.trailingZeroByteCount } + if (lowHalf .== .zero) != all_zero { return 4 + lowHalf.trailingZeroByteCount } + return scalarCount + } +} +public extension SIMD16 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingZeroByteCount : Int { + let all_zero:SIMDMask> = .init(repeating: true) + if (highHalf .== .zero) != all_zero { return highHalf.trailingZeroByteCount } + if (lowHalf .== .zero) != all_zero { return 8 + lowHalf.trailingZeroByteCount } + return scalarCount + } +} +public extension SIMD32 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingZeroByteCount : Int { + let all_zero:SIMDMask> = .init(repeating: true) + if (highHalf .== .zero) != all_zero { return highHalf.trailingZeroByteCount } + if (lowHalf .== .zero) != all_zero { return 16 + lowHalf.trailingZeroByteCount } + return scalarCount + } +} +public extension SIMD64 where Scalar : BinaryInteger { + /// - Complexity: O(1) + @inlinable + var trailingZeroByteCount : Int { + let all_zero:SIMDMask> = .init(repeating: true) + if (highHalf .== .zero) != all_zero { return highHalf.trailingZeroByteCount } + if (lowHalf .== .zero) != all_zero { return 32 + lowHalf.trailingZeroByteCount } + return scalarCount + } +} + // MARK: hasPrefix O(1) // implementation should never change public extension SIMD4 where Scalar : BinaryInteger { diff --git a/Sources/DestinyUtilities/Utilities.swift b/Sources/DestinyUtilities/Utilities.swift index 918ef1c..e9e71c8 100644 --- a/Sources/DestinyUtilities/Utilities.swift +++ b/Sources/DestinyUtilities/Utilities.swift @@ -29,6 +29,7 @@ public typealias DestinyRoutePathType = StackString32 // MARK: Request public struct Request : ~Copyable { + public let token:DestinyRoutePathType public let method:HTTPRequest.Method public let path:[String] public let version:String @@ -36,12 +37,14 @@ public struct Request : ~Copyable { public let body:String public init( + token: DestinyRoutePathType, method: HTTPRequest.Method, path: [String], version: String, headers: [String:String], body: String ) { + self.token = token self.method = method self.path = path self.version = version diff --git a/Tests/DestinyTests/DestinyTests.swift b/Tests/DestinyTests/DestinyTests.swift index 479c403..e503ccd 100644 --- a/Tests/DestinyTests/DestinyTests.swift +++ b/Tests/DestinyTests/DestinyTests.swift @@ -28,6 +28,40 @@ struct DestinyTests { ss2 = StackString2(&string) #expect(ss2.leadingNonzeroByteCount == 1) } + @Test func simd_trailingNonzeroByteCount() { + var string:String = "siuerbnieprsbgsrgnpeirfnpae" + var ss32:StackString32 = StackString32(&string) + #expect(ss32.trailingNonzeroByteCount == 0) + + string = "ouerbgouwrgoruegbrotugbtg\0enrotg" + ss32 = StackString32(&string) + #expect(ss32.trailingNonzeroByteCount == 6) + + string = "" + var ss2:StackString2 = StackString2(&string) + #expect(ss2.trailingNonzeroByteCount == 0) + + string = "a" + ss2 = StackString2(&string) + #expect(ss2.trailingNonzeroByteCount == 0) + } + @Test func simd_trailingZeroByteCount() { + var string:String = "siuerbnieprsbgsrgnpeirfnpae" + var ss32:StackString32 = StackString32(&string) + #expect(ss32.trailingZeroByteCount == 5) + + string = "ouerbgouwrgoruegbrotugbtg\0enrotg" + ss32 = StackString32(&string) + #expect(ss32.trailingZeroByteCount == 0) + + string = "" + var ss2:StackString2 = StackString2(&string) + #expect(ss2.trailingZeroByteCount == 2, Comment(rawValue: ss2.string())) + + string = "a" + ss2 = StackString2(&string) + #expect(ss2.trailingZeroByteCount == 1, Comment(rawValue: ss2.string())) + } @Test func simd_hasPrefix() { var string:String = "testing brother!?!" let test:StackString32 = StackString32(&string) @@ -93,55 +127,39 @@ struct DestinyTests { #expect(ss.string() == "brooooooooo !") } @Test func example() { - let static_string_router:Router = #router( + let _:Router = #router( version: "HTTP/2.0", middleware: [ - StaticMiddleware(handlesMethods: [.get], handlesContentTypes: [.html], appliesStatus: .ok, appliesHeaders: ["Are-You-My-Brother":"yes"]) + StaticMiddleware(handlesMethods: [.get], handlesContentTypes: [HTTPMediaType.Text.html], appliesStatus: .ok, appliesHeaders: ["Are-You-My-Brother":"yes"]) ], StaticRoute( method: .get, - path: ["test"], - contentType: .html, + path: ["test1"], + contentType: HTTPMediaType.Text.html, charset: "UTF-8", result: .string("This outcome was inevitable; 'twas your destiny") - ) - ) - let static_string_router2:Router = #router( - version: "HTTP/2.0", - middleware: [ - StaticMiddleware(handlesMethods: [.get], handlesContentTypes: [.html], appliesStatus: .ok, appliesHeaders: ["Are-You-My-Brother":"yes"]) - ], + ), StaticRoute( method: .get, - path: ["test"], + path: ["test2"], status: .movedPermanently, - contentType: .html, + contentType: HTTPMediaType.Text.html, charset: "UTF-8", result: .string("This outcome was inevitable; 'twas your destiny") - ) - ) - let uint8Array_router:Router = #router( - version: "HTTP/2.0", - middleware: [ - StaticMiddleware(handlesMethods: [.get], handlesContentTypes: [.html], appliesStatus: .ok, appliesHeaders: ["Are-You-My-Brother":"yes"]) - ], + ), StaticRoute( + returnType: .uint8Array, method: .get, - path: ["test"], - contentType: .html, + path: ["test3"], + contentType: HTTPMediaType.Text.html, charset: "UTF-8", result: .string("This outcome was inevitable; 'twas your destiny") - ) - ) - let uint16Array_router:Router = #router( - version: "HTTP/2.0", - middleware: [ - StaticMiddleware(handlesMethods: [.get], handlesContentTypes: [.html], appliesStatus: .ok, appliesHeaders: ["Are-You-My-Brother":"yes"]) - ], + ), StaticRoute( + returnType: .uint16Array, method: .get, path: ["test"], - contentType: .html, + contentType: HTTPMediaType.Text.html, charset: "UTF-8", result: .string("This outcome was inevitable; 'twas your destiny") )