Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Directly parse memory without ByteBuffer #442

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 34 additions & 30 deletions Sources/Hummingbird/Router/BinaryTrie/BinaryTrie+resolve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,35 @@ extension BinaryTrie {
/// Resolve a path to a `Value` if available
@inlinable
@_spi(Internal) public func resolve(_ path: String) -> (value: Value, parameters: Parameters)? {
var trie = trie
let pathComponents = path.split(separator: "/", omittingEmptySubsequences: true)
var pathComponentsIterator = pathComponents.makeIterator()
var parameters = Parameters()
guard var node: BinaryTrieNode = trie.readBinaryTrieNode() else { return nil }
while let component = pathComponentsIterator.next() {
node = self.matchComponent(component, in: &trie, parameters: &parameters)
if node.token == .recursiveWildcard {
// we have found a recursive wildcard. Go through all the path components until we match one of them
// or reach the end of the path component array
var range = component.startIndex..<component.endIndex
while let component = pathComponentsIterator.next() {
var recursiveTrie = trie
let recursiveNode = self.matchComponent(component, in: &recursiveTrie, parameters: &parameters)
if recursiveNode.token != .deadEnd {
node = recursiveNode
break
return trie.withParsingContext { trie in
let pathComponents = path.split(separator: "/", omittingEmptySubsequences: true)
var pathComponentsIterator = pathComponents.makeIterator()
var parameters = Parameters()
var node: BinaryTrieNode = trie.readBinaryTrieNode()
while let component = pathComponentsIterator.next() {
node = self.matchComponent(component, in: &trie, parameters: &parameters)
if node.token == .recursiveWildcard {
// we have found a recursive wildcard. Go through all the path components until we match one of them
// or reach the end of the path component array
var range = component.startIndex..<component.endIndex
while let component = pathComponentsIterator.next() {
var recursiveTrie = trie
let recursiveNode = self.matchComponent(component, in: &recursiveTrie, parameters: &parameters)
if recursiveNode.token != .deadEnd {
node = recursiveNode
break
}
// extend range of catch all text
range = range.lowerBound..<component.endIndex
}
// extend range of catch all text
range = range.lowerBound..<component.endIndex
parameters.setCatchAll(path[range])
}
if node.token == .deadEnd {
return nil
}
parameters.setCatchAll(path[range])
}
if node.token == .deadEnd {
return nil
}
return self.value(for: node.index, parameters: parameters)
}
return self.value(for: node.index, parameters: parameters)
}

/// If `index != nil`, resolves the `index` to a `Value`
Expand All @@ -63,20 +64,21 @@ extension BinaryTrie {
@usableFromInline
internal func matchComponent(
_ component: Substring,
in trie: inout ByteBuffer,
in trie: inout Trie.ParsingContext,
parameters: inout Parameters
) -> BinaryTrieNode {
while let node = trie.readBinaryTrieNode() {
while !trie.isAtEnd {
let node = trie.readBinaryTrieNode()
let result = self.matchComponent(component, withToken: node.token, in: &trie, parameters: &parameters)
switch result {
case .match, .deadEnd:
return node
default:
trie.moveReaderIndex(to: Int(node.nextSiblingNodeIndex))
trie.byteOffset = Int(node.nextSiblingNodeIndex)
}
}
// should never get here
return .init(index: 0, token: .deadEnd, nextSiblingNodeIndex: UInt32(trie.writerIndex))
return .init(index: 0, token: .deadEnd, nextSiblingNodeIndex: UInt32(trie.buffer.count))
}

@usableFromInline
Expand All @@ -88,7 +90,7 @@ extension BinaryTrie {
internal func matchComponent(
_ component: Substring,
withToken token: BinaryTrieTokenKind,
in trie: inout ByteBuffer,
in trie: inout Trie.ParsingContext,
parameters: inout Parameters
) -> MatchResult {
switch token {
Expand All @@ -107,7 +109,9 @@ extension BinaryTrie {
case .capture:
// The current node is a parameter
guard
let parameter = trie.readLengthPrefixedString(as: Integer.self)
let parameter = trie.readLengthPrefixedString(
as: Integer.self
)
else {
return .mismatch
}
Expand Down
70 changes: 68 additions & 2 deletions Sources/Hummingbird/Router/BinaryTrie/BinaryTrie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,61 @@ struct BinaryTrieNode {
static let serializedSize = 7
}

@usableFromInline
struct Trie: @unchecked Sendable {
@usableFromInline
let trie: ManagedBuffer<Void, UInt8>

@inlinable
init(trie: ManagedBuffer<Void, UInt8>) {
self.trie = trie
}

@usableFromInline
func withParsingContext<T>(
_ perform: (inout ParsingContext) throws -> T
) rethrows -> T {
let byteSize = trie.capacity
return try trie.withUnsafeMutablePointerToElements { pointer in
var context = ParsingContext(
buffer: UnsafeRawBufferPointer(
start: pointer,
count: byteSize
),
byteOffset: 0
)

return try perform(&context)
}
}

@usableFromInline
struct ParsingContext {
@usableFromInline
let buffer: UnsafeRawBufferPointer

@usableFromInline
var byteOffset: Int

@usableFromInline
init(buffer: UnsafeRawBufferPointer, byteOffset: Int) {
self.buffer = buffer
self.byteOffset = byteOffset
}

@usableFromInline
var isAtEnd: Bool {
byteOffset >= buffer.count
}
}
}

@_spi(Internal) public final class BinaryTrie<Value: Sendable>: Sendable {
@usableFromInline
typealias Integer = UInt8

@usableFromInline
let trie: ByteBuffer
let trie: Trie

@usableFromInline
let values: [Value?]
Expand All @@ -55,7 +104,24 @@ struct BinaryTrieNode {
values: &values
)

self.trie = trie
let buffer = ManagedBuffer<Void, UInt8>.create(
minimumCapacity: trie.readableBytes
) { managedBuffer in
return ()
}

buffer.withUnsafeMutablePointerToElements { destination in
trie.withUnsafeReadableBytes { source in
_ = source.copyBytes(
to: UnsafeMutableRawBufferPointer(
start: destination,
count: source.count
)
)
}
}

self.trie = Trie(trie: buffer)
self.values = values
}
}
89 changes: 46 additions & 43 deletions Sources/Hummingbird/Router/BinaryTrie/ByteBuffer+BinaryTrie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import Glibc

internal extension ByteBuffer {
/// Write length prefixed string to ByteBuffer
@inlinable
mutating func writeLengthPrefixedString<F: FixedWidthInteger>(_ string: Substring, as integer: F.Type) {
do {
try self.writeLengthPrefixed(as: F.self) { buffer in
try self.writeLengthPrefixed(endianness: .host, as: F.self) { buffer in
buffer.writeSubstring(string)
}
} catch {
Expand All @@ -36,9 +37,9 @@ internal extension ByteBuffer {

/// Write BinaryTrieNode into ByteBuffer at position
@discardableResult mutating func setBinaryTrieNode(_ node: BinaryTrieNode, at index: Int) -> Int {
var offset = self.setInteger(node.index, at: index)
offset += self.setInteger(node.token.rawValue, at: index + offset)
offset += self.setInteger(node.nextSiblingNodeIndex, at: index + offset)
var offset = self.setInteger(node.index, at: index, endianness: .host)
offset += self.setInteger(node.token.rawValue, at: index + offset, endianness: .host)
offset += self.setInteger(node.nextSiblingNodeIndex, at: index + offset, endianness: .host)
return offset
}

Expand All @@ -52,31 +53,32 @@ internal extension ByteBuffer {
mutating func reserveBinaryTrieNode() {
self.moveWriterIndex(forwardBy: BinaryTrieNode.serializedSize)
}
}

extension Trie.ParsingContext {
/// Read BinaryTrieNode from ByteBuffer
@usableFromInline
mutating func readBinaryTrieNode() -> BinaryTrieNode? {
guard let index = self.readInteger(as: UInt16.self),
let token = self.readToken(),
let nextSiblingNodeIndex: UInt32 = self.readInteger()
else {
return nil
}
mutating func readBinaryTrieNode() -> BinaryTrieNode {
let index = buffer.loadUnaligned(fromByteOffset: byteOffset, as: UInt16.self)
byteOffset &+= 2

let token = buffer.loadUnaligned(fromByteOffset: byteOffset, as: BinaryTrieTokenKind.self)
byteOffset &+= 1

let nextSiblingNodeIndex = buffer.loadUnaligned(fromByteOffset: byteOffset, as: UInt32.self)
byteOffset &+= 4

return BinaryTrieNode(index: index, token: token, nextSiblingNodeIndex: nextSiblingNodeIndex)
}

/// Read string from ByteBuffer and compare against another string
@inlinable
mutating func readAndCompareString<Length: FixedWidthInteger>(
to string: Substring,
length: Length.Type
) -> Bool {
guard
let _length: Length = readInteger()
else {
return false
}

let length = Int(_length)
let length = Int(buffer.loadUnaligned(fromByteOffset: byteOffset, as: Length.self))
byteOffset &+= Length.bitWidth / 8

func compare(utf8: UnsafeBufferPointer<UInt8>) -> Bool {
if utf8.count != length {
Expand All @@ -89,13 +91,15 @@ internal extension ByteBuffer {
return true
}

return withUnsafeReadableBytes { buffer in
if memcmp(utf8.baseAddress!, buffer.baseAddress!, length) == 0 {
moveReaderIndex(forwardBy: length)
return true
} else {
return false
}
if memcmp(
utf8.baseAddress!,
buffer.baseAddress!.advanced(by: byteOffset),
length
) == 0 {
byteOffset &+= length
return true
} else {
return false
}
}

Expand All @@ -114,23 +118,22 @@ internal extension ByteBuffer {
}

/// Read length prefixed string from ByteBuffer
mutating func readLengthPrefixedString<F: FixedWidthInteger>(as integer: F.Type) -> String? {
guard let buffer = readLengthPrefixedSlice(as: F.self) else {
return nil
}

return String(buffer: buffer)
}

/// Read BinaryTrieTokenKind from ByteBuffer
mutating func readToken() -> BinaryTrieTokenKind? {
guard
let _token: BinaryTrieTokenKind.RawValue = readInteger(),
let token = BinaryTrieTokenKind(rawValue: _token)
else {
return nil
}

return token
@inlinable
mutating func readLengthPrefixedString<F: FixedWidthInteger>(
as integer: F.Type
) -> String? {
let lengthPrefix = buffer.loadUnaligned(fromByteOffset: byteOffset, as: F.self)
byteOffset &+= (F.bitWidth / 8)
let string = String(
bytes: UnsafeRawBufferPointer(
start: buffer.baseAddress!.advanced(
by: byteOffset
),
count: Int(lengthPrefix)
),
encoding: .utf8
)
byteOffset &+= Int(lengthPrefix)
return string
}
}
Loading