Skip to content

Commit

Permalink
Struct of Arrays approach
Browse files Browse the repository at this point in the history
  • Loading branch information
Joannis committed May 8, 2024
1 parent 3531f5c commit 93746a3
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 203 deletions.
84 changes: 50 additions & 34 deletions Sources/Hummingbird/Router/BinaryTrie/BinaryTrie+resolve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ import NIOCore
extension BinaryTrie {
/// Resolve a path to a `Value` if available
@_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 }

var node = trie.nodes[0]
var nodeIndex = 1

while let component = pathComponentsIterator.next() {
node = self.matchComponent(component, in: &trie, parameters: &parameters)
node = self.matchComponent(component, atNodeIndex: &nodeIndex, 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)
var nodeIndex = nodeIndex
let recursiveNode = self.matchComponent(component, atNodeIndex: &nodeIndex, parameters: &parameters)
if recursiveNode.token != .deadEnd {
node = recursiveNode
break
Expand All @@ -44,7 +46,8 @@ extension BinaryTrie {
return nil
}
}
return self.value(for: node.index, parameters: parameters)

return self.value(for: node.valueIndex, parameters: parameters)
}

/// If `index != nil`, resolves the `index` to a `Value`
Expand All @@ -60,20 +63,27 @@ extension BinaryTrie {
/// Match sibling node for path component
private func matchComponent(
_ component: Substring,
in trie: inout ByteBuffer,
atNodeIndex nodeIndex: inout Int,
parameters: inout Parameters
) -> BinaryTrieNode {
while let node = trie.readBinaryTrieNode() {
let result = self.matchComponent(component, withToken: node.token, in: &trie, parameters: &parameters)
while nodeIndex < trie.nodes.count {
let node = trie.nodes[nodeIndex]
let result = self.matchComponent(
component,
node: node,
parameters: &parameters
)
switch result {
case .match, .deadEnd:
nodeIndex += 1
return node
default:
trie.moveReaderIndex(to: Int(node.nextSiblingNodeIndex))
nodeIndex = Int(node.nextSiblingNodeIndex)
}
}

// should never get here
return .init(index: 0, token: .deadEnd, nextSiblingNodeIndex: UInt32(trie.writerIndex))
return BinaryTrieNode(valueIndex: 0, token: .deadEnd, nextSiblingNodeIndex: .max)
}

private enum MatchResult {
Expand All @@ -82,71 +92,77 @@ extension BinaryTrie {

private func matchComponent(
_ component: Substring,
withToken token: BinaryTrieTokenKind,
in trie: inout ByteBuffer,
node: BinaryTrieNode,
parameters: inout Parameters
) -> MatchResult {
switch token {
switch node.token {
case .path:
// The current node is a constant
guard
trie.readAndCompareString(
to: component,
length: Integer.self
)
let constant = node.constant,
trie.constants[Int(constant)] == component
else {
return .mismatch
}

return .match
case .capture:
// The current node is a parameter
guard
let parameter = trie.readLengthPrefixedString(as: Integer.self)
else {
guard let parameter = node.parameter else {
return .mismatch
}

parameters[Substring(parameter)] = component
parameters[trie.parameters[Int(parameter)]] = component
return .match
case .prefixCapture:
guard
let suffix = trie.readLengthPrefixedString(as: Integer.self),
let parameter = trie.readLengthPrefixedString(as: Integer.self),
component.hasSuffix(suffix)
let constant = node.constant,
let parameter = node.parameter
else {
return .mismatch
}

parameters[Substring(parameter)] = component.dropLast(suffix.count)
let suffix = trie.constants[Int(constant)]

guard component.hasSuffix(suffix) else {
return .mismatch
}

parameters[trie.parameters[Int(parameter)]] = component.dropLast(suffix.count)
return .match
case .suffixCapture:
guard
let prefix = trie.readLengthPrefixedString(as: Integer.self),
let parameter = trie.readLengthPrefixedString(as: Integer.self),
component.hasPrefix(prefix)
let constant = node.constant,
let parameter = node.parameter,
component.hasPrefix(trie.constants[Int(constant)])
else {
return .mismatch
}

parameters[Substring(parameter)] = component.dropFirst(prefix.count)
let prefix = trie.constants[Int(constant)]

guard component.hasPrefix(prefix) else {
return .mismatch
}

parameters[trie.parameters[Int(parameter)]] = component.dropFirst(prefix.count)
return .match
case .wildcard:
// Always matches, descend
return .match
case .prefixWildcard:
guard
let suffix = trie.readLengthPrefixedString(as: Integer.self),
component.hasSuffix(suffix)
let constant = node.constant,
component.hasSuffix(trie.constants[Int(constant)])
else {
return .mismatch
}

return .match
case .suffixWildcard:
guard
let prefix = trie.readLengthPrefixedString(as: Integer.self),
component.hasPrefix(prefix)
let constant = node.constant,
component.hasPrefix(trie.constants[Int(constant)])
else {
return .mismatch
}
Expand Down
88 changes: 59 additions & 29 deletions Sources/Hummingbird/Router/BinaryTrie/BinaryTrie+serialize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,69 +17,99 @@ import NIOCore
extension BinaryTrie {
static func serialize(
_ node: RouterPathTrieBuilder<Value>.Node,
trie: inout ByteBuffer,
trie: inout Trie,
values: inout [Value?]
) {
let binaryTrieNodeIndex = trie.writerIndex
trie.reserveBinaryTrieNode()
// Index where `value` is located
let index = UInt16(values.count)
let valueIndex = UInt16(values.count)
values.append(node.value)

let token: BinaryTrieTokenKind
let constant: UInt16?
let parameter: UInt16?

func setConstant(_ constant: Substring) -> UInt16 {
if let index = trie.constants.firstIndex(of: constant) {
return UInt16(index)
} else {
let index = UInt16(trie.constants.count)
trie.constants.append(constant)
return index
}
}

func setParameter(_ parameter: Substring) -> UInt16 {
if let index = trie.parameters.firstIndex(of: parameter) {
return UInt16(index)
} else {
let index = UInt16(trie.parameters.count)
trie.parameters.append(parameter)
return index
}
}

switch node.key {
case .path(let path):
token = .path
// Serialize the path constant
trie.writeLengthPrefixedString(path, as: Integer.self)
case .capture(let parameter):
constant = setConstant(path)
parameter = nil
case .capture(let parameterName):
token = .capture
// Serialize the parameter
trie.writeLengthPrefixedString(parameter, as: Integer.self)
case .prefixCapture(suffix: let suffix, parameter: let parameter):
constant = nil
parameter = setParameter(parameterName)
case .prefixCapture(suffix: let suffix, parameter: let parameterName):
token = .prefixCapture
// Serialize the suffix and parameter
trie.writeLengthPrefixedString(suffix, as: Integer.self)
trie.writeLengthPrefixedString(parameter, as: Integer.self)
case .suffixCapture(prefix: let prefix, parameter: let parameter):
constant = setConstant(suffix)
parameter = setParameter(parameterName)
case .suffixCapture(prefix: let prefix, parameter: let parameterName):
token = .suffixCapture
// Serialize the prefix and parameter
trie.writeLengthPrefixedString(prefix, as: Integer.self)
trie.writeLengthPrefixedString(parameter, as: Integer.self)
constant = setConstant(prefix)
parameter = setParameter(parameterName)
case .wildcard:
token = .wildcard
constant = nil
parameter = nil
case .prefixWildcard(let suffix):
token = .prefixWildcard
// Serialize the suffix
trie.writeLengthPrefixedString(suffix, as: Integer.self)
constant = setConstant(suffix)
parameter = nil
case .suffixWildcard(let prefix):
token = .suffixWildcard
// Serialize the prefix
trie.writeLengthPrefixedString(prefix, as: Integer.self)
constant = setConstant(prefix)
parameter = nil
case .recursiveWildcard:
token = .recursiveWildcard
constant = nil
parameter = nil
case .null:
token = .null
constant = nil
parameter = nil
}

let nodeIndex = trie.nodes.count
trie.nodes.append(
BinaryTrieNode(
valueIndex: valueIndex,
token: token,
nextSiblingNodeIndex: .max,
constant: constant,
parameter: parameter
)
)

self.serializeChildren(
of: node,
trie: &trie,
values: &values
)

let deadEndIndex = trie.writerIndex
// The last node in a trie is always a deadEnd token. We reserve space for it so we
// get the correct writer index for the next sibling
trie.reserveBinaryTrieNode()
trie.setBinaryTrieNode(.init(index: 0, token: .deadEnd, nextSiblingNodeIndex: UInt32(trie.writerIndex)), at: deadEndIndex)
// Write trie node
trie.setBinaryTrieNode(.init(index: index, token: token, nextSiblingNodeIndex: UInt32(trie.writerIndex)), at: binaryTrieNodeIndex)
trie.nodes[nodeIndex].nextSiblingNodeIndex = UInt16(trie.nodes.count)
}

static func serializeChildren(
of node: RouterPathTrieBuilder<Value>.Node,
trie: inout ByteBuffer,
trie: inout Trie,
values: inout [Value?]
) {
// Serialize the child nodes in order of priority
Expand Down
26 changes: 21 additions & 5 deletions Sources/Hummingbird/Router/BinaryTrie/BinaryTrie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,30 @@ enum BinaryTrieTokenKind: UInt8 {
case deadEnd
}

struct BinaryTrieNode {
let index: UInt16
struct BinaryTrieNode: Sendable {
let valueIndex: UInt16
let token: BinaryTrieTokenKind
let nextSiblingNodeIndex: UInt32
var nextSiblingNodeIndex: UInt16
var constant: UInt16?
var parameter: UInt16?

/// How many bytes a serialized BinaryTrieNode uses
static let serializedSize = 7
}

struct Trie: Sendable {
var nodes = [BinaryTrieNode]()
var parameters = [Substring]()
var constants = [Substring]()
}

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

@_spi(Internal) public init(base: RouterPathTrieBuilder<Value>) {
var trie = ByteBufferAllocator().buffer(capacity: 1024)
var trie = Trie()
var values: [Value?] = []

Self.serialize(
Expand All @@ -42,6 +50,14 @@ struct BinaryTrieNode {
values: &values
)

trie.nodes.append(
BinaryTrieNode(
valueIndex: 0,
token: .deadEnd,
nextSiblingNodeIndex: .max
)
)

self.trie = trie
self.values = values
}
Expand Down
Loading

0 comments on commit 93746a3

Please sign in to comment.