Skip to content

Commit

Permalink
Apply some of Adam's suggestions to make BinaryTrie code more readable
Browse files Browse the repository at this point in the history
  • Loading branch information
Joannis committed Apr 11, 2024
1 parent b2bf10e commit a75ae59
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 51 deletions.
4 changes: 1 addition & 3 deletions Benchmarks/Benchmarks/HTTP1/HTTP1ChannelBenchmarks.swift
Expand Up @@ -51,9 +51,7 @@ extension Benchmark {
try HTTP1Channel.Value(wrappingChannelSynchronously: channel)
}.get()
task = Task {
await withDiscardingTaskGroup { taskGroup in
http1.handle(value: asyncChannel, logger: Logger(label: "Testing"), onTaskGroup: &taskGroup)
}
await http1.handle(value: asyncChannel, logger: Logger(label: "Testing"))
}
} teardown: {
try await channel.close()
Expand Down
116 changes: 84 additions & 32 deletions Sources/Hummingbird/Router/BinaryTrie/BinaryTrie+resolve.swift
Expand Up @@ -18,7 +18,7 @@ extension BinaryTrie {
/// Resolve a path to a `Value` if available
@_spi(Internal) public func resolve(_ path: String) -> (value: Value, parameters: Parameters)? {
var trie = trie
var pathComponents = path.split(separator: "/", omittingEmptySubsequences: true)
var pathComponents = path.split(separator: "/", omittingEmptySubsequences: true)[...]
var parameters = Parameters()

if pathComponents.isEmpty {
Expand Down Expand Up @@ -49,17 +49,19 @@ extension BinaryTrie {
}

private func matchComponent(
_ component: inout Substring,
withToken token: TokenKind,
_ component: Substring,
withToken token: BinaryTrieTokenKind,
in trie: inout ByteBuffer,
parameters: inout Parameters
) -> MatchResult {
switch token {
case .path:
// The current node is a constant
guard
let length: Integer = trie.readInteger(),
trie.readAndCompareString(to: &component, length: length)
trie.readAndCompareString(
to: component,
length: Integer.self
)
else {
return .mismatch
}
Expand All @@ -68,8 +70,7 @@ extension BinaryTrie {
case .capture:
// The current node is a parameter
guard
let length: Integer = trie.readInteger(),
let parameter = trie.readString(length: Int(length))
let parameter = trie.readLengthPrefixedString(as: Integer.self)
else {
return .mismatch
}
Expand All @@ -78,39 +79,32 @@ extension BinaryTrie {
return .match
case .prefixCapture:
guard
let suffixLength: Integer = trie.readInteger(),
let suffix = trie.readString(length: Int(suffixLength)),
let parameterLength: Integer = trie.readInteger(),
let parameter = trie.readString(length: Int(parameterLength)),
let suffix = trie.readLengthPrefixedString(as: Integer.self),
let parameter = trie.readLengthPrefixedString(as: Integer.self),
component.hasSuffix(suffix)
else {
return .mismatch
}

component.removeLast(suffix.count)
parameters[Substring(parameter)] = component
parameters[Substring(parameter)] = component.dropLast(suffix.count)
return .match
case .suffixCapture:
guard
let prefixLength: Integer = trie.readInteger(),
let prefix = trie.readString(length: Int(prefixLength)),
let parameterLength: Integer = trie.readInteger(),
let parameter = trie.readString(length: Int(parameterLength)),
let prefix = trie.readLengthPrefixedString(as: Integer.self),
let parameter = trie.readLengthPrefixedString(as: Integer.self),
component.hasPrefix(prefix)
else {
return .mismatch
}

component.removeFirst(Int(prefixLength))
parameters[Substring(parameter)] = component
parameters[Substring(parameter)] = component.dropFirst(prefix.count)
return .match
case .wildcard:
// Always matches, descend
return .match
case .prefixWildcard:
guard
let suffixLength: Integer = trie.readInteger(),
let suffix = trie.readString(length: Int(suffixLength)),
let suffix = trie.readLengthPrefixedString(as: Integer.self),
component.hasSuffix(suffix)
else {
return .mismatch
Expand All @@ -119,8 +113,7 @@ extension BinaryTrie {
return .match
case .suffixWildcard:
guard
let prefixLength: Integer = trie.readInteger(),
let prefix = trie.readString(length: Int(prefixLength)),
let prefix = trie.readLengthPrefixedString(as: Integer.self),
component.hasPrefix(prefix)
else {
return .mismatch
Expand All @@ -141,7 +134,7 @@ extension BinaryTrie {
in trie: inout ByteBuffer,
index: UInt16,
parameters: inout Parameters,
components: inout [Substring],
components: inout ArraySlice<Substring>,
isInRecursiveWildcard: Bool
) -> (value: Value, parameters: Parameters)? {
// If there are no more components in the path, return the value found
Expand All @@ -154,18 +147,17 @@ extension BinaryTrie {

// Check the current node type through TokenKind
// And read the location of the _next_ node from the trie buffer
while
while
let index = trie.readInteger(as: UInt16.self),
let _token: Integer = trie.readInteger(),
let token = TokenKind(rawValue: _token),
let token = trie.readToken(),
let nextSiblingNodeIndex: UInt32 = trie.readInteger()
{
repeat {
// Record the current readerIndex
// ``matchComponent`` moves the reader index forward, so we'll need to reset it
// If we're in a recursiveWildcard and this component does not match
let readerIndex = trie.readerIndex
let result = matchComponent(&component, withToken: token, in: &trie, parameters: &parameters)
let result = matchComponent(component, withToken: token, in: &trie, parameters: &parameters)

switch result {
case .match:
Expand All @@ -181,7 +173,9 @@ extension BinaryTrie {
return nil
}

component = components.removeFirst()
component = components[components.startIndex]
components = components.dropFirst()

// Move back he readerIndex, so that we can retry this step again with
// the next component
trie.moveReaderIndex(to: readerIndex)
Expand Down Expand Up @@ -220,9 +214,19 @@ import Glibc
#endif

fileprivate extension ByteBuffer {
mutating func readAndCompareString<Length: FixedWidthInteger>(to string: inout Substring, length: Length) -> Bool {
let length = Int(length)
return string.withUTF8 { utf8 in
mutating func readAndCompareString<Length: FixedWidthInteger>(
to string: Substring,
length: Length.Type
) -> Bool {
guard
let _length: Length = readInteger()
else {
return false
}

let length = Int(_length)

func compare(utf8: UnsafeBufferPointer<UInt8>) -> Bool {
if utf8.count != length {
return false
}
Expand All @@ -242,5 +246,53 @@ fileprivate extension ByteBuffer {
}
}
}

guard let result = string.withContiguousStorageIfAvailable({ characters in
characters.withMemoryRebound(to: UInt8.self) { utf8 in
compare(utf8: utf8)
}
}) else {
var string = string
return string.withUTF8 { utf8 in
compare(utf8: utf8)
}
}

return result
}

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

return String(buffer: buffer)
}

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

return token
}

mutating func readBinaryTrieNode() -> BinaryTrieNode? {
guard
let index = readInteger(as: UInt16.self),
let token = readToken(),
let nextSiblingNodeIndex: UInt32 = readInteger()
else {
return nil
}

return BinaryTrieNode(
index: index,
kind: token,
nextSiblingNodeIndex: nextSiblingNodeIndex
)
}
}
26 changes: 16 additions & 10 deletions Sources/Hummingbird/Router/BinaryTrie/BinaryTrie+serialize.swift
Expand Up @@ -36,23 +36,23 @@ extension BinaryTrie {
// Serialize the node's component
switch node.key {
case .path(let path):
trie.writeInteger(TokenKind.path.rawValue)
trie.writeToken(.path)
nextNodeOffsetIndex = reserveUInt32()

// Serialize the path constant
try trie.writeLengthPrefixed(as: Integer.self) { buffer in
buffer.writeSubstring(path)
}
case .capture(let parameter):
trie.writeInteger(TokenKind.capture.rawValue)
trie.writeToken(.capture)
nextNodeOffsetIndex = reserveUInt32()

// Serialize the parameter
try trie.writeLengthPrefixed(as: Integer.self) { buffer in
buffer.writeSubstring(parameter)
}
case .prefixCapture(suffix: let suffix, parameter: let parameter):
trie.writeInteger(TokenKind.prefixCapture.rawValue)
trie.writeToken(.prefixCapture)
nextNodeOffsetIndex = reserveUInt32()

// Serialize the suffix and parameter
Expand All @@ -63,7 +63,7 @@ extension BinaryTrie {
buffer.writeSubstring(parameter)
}
case .suffixCapture(prefix: let prefix, parameter: let parameter):
trie.writeInteger(TokenKind.suffixCapture.rawValue)
trie.writeToken(.suffixCapture)
nextNodeOffsetIndex = reserveUInt32()

// Serialize the prefix and parameter
Expand All @@ -74,29 +74,29 @@ extension BinaryTrie {
buffer.writeSubstring(parameter)
}
case .wildcard:
trie.writeInteger(TokenKind.wildcard.rawValue)
trie.writeToken(.wildcard)
nextNodeOffsetIndex = reserveUInt32()
case .prefixWildcard(let suffix):
trie.writeInteger(TokenKind.prefixWildcard.rawValue)
trie.writeToken(.prefixWildcard)
nextNodeOffsetIndex = reserveUInt32()

// Serialize the suffix
try trie.writeLengthPrefixed(as: Integer.self) { buffer in
buffer.writeSubstring(suffix)
}
case .suffixWildcard(let prefix):
trie.writeInteger(TokenKind.suffixWildcard.rawValue)
trie.writeToken(.suffixWildcard)
nextNodeOffsetIndex = reserveUInt32()

// Serialize the prefix
try trie.writeLengthPrefixed(as: Integer.self) { buffer in
buffer.writeSubstring(prefix)
}
case .recursiveWildcard:
trie.writeInteger(TokenKind.recursiveWildcard.rawValue)
trie.writeToken(.recursiveWildcard)
nextNodeOffsetIndex = reserveUInt32()
case .null:
trie.writeInteger(TokenKind.null.rawValue)
trie.writeToken(.null)
nextNodeOffsetIndex = reserveUInt32()
}

Expand All @@ -108,7 +108,7 @@ extension BinaryTrie {

// The last node in a trie is always a null token
// Since there is no next node to check anymores
trie.writeInteger(TokenKind.deadEnd.rawValue)
trie.writeToken(.deadEnd)

// Write the offset of the next node, always immediately after this node
// Write a `deadEnd` at the end of this node, and update the current node in case
Expand Down Expand Up @@ -159,3 +159,9 @@ extension RouterPath.Element {
}
}
}

fileprivate extension ByteBuffer {
mutating func writeToken(_ token: BinaryTrieTokenKind) {
writeInteger(token.rawValue)
}
}
12 changes: 6 additions & 6 deletions Sources/Hummingbird/Router/BinaryTrie/BinaryTrie.swift
Expand Up @@ -12,17 +12,17 @@
//
//===----------------------------------------------------------------------===//

enum BinaryTrieTokenKind: UInt8 {
case null = 0
case path, capture, prefixCapture, suffixCapture, wildcard, prefixWildcard, suffixWildcard, recursiveWildcard
case deadEnd
}

@_spi(Internal) public final class BinaryTrie<Value: Sendable>: Sendable {
typealias Integer = UInt8
let trie: ByteBuffer
let values: [Value?]

enum TokenKind: UInt8 {
case null = 0
case path, capture, prefixCapture, suffixCapture, wildcard, prefixWildcard, suffixWildcard, recursiveWildcard
case deadEnd
}

@_spi(Internal) public init(base: RouterPathTrie<Value>) throws {
var trie = ByteBufferAllocator().buffer(capacity: 1024)
var values = [base.root.value]
Expand Down

0 comments on commit a75ae59

Please sign in to comment.