Skip to content

Commit

Permalink
Even faster!
Browse files Browse the repository at this point in the history
  • Loading branch information
Joannis committed May 9, 2024
1 parent a3ff4ec commit 1f6d21b
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 121 deletions.
2 changes: 1 addition & 1 deletion Benchmarks/Benchmarks/Router/Benchmarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ let benchmarks = {
],
warmupIterations: 10
)
binaryTrieRouterBenchmarks()
routerTrieRouterBenchmarks()
routerBenchmarks()
}
20 changes: 10 additions & 10 deletions Benchmarks/Benchmarks/Router/BinaryTrieRouterBenchmarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
import Benchmark
@_spi(Internal) import Hummingbird

func binaryTrieRouterBenchmarks() {
var trie: BinaryTrie<String>!
Benchmark("BinaryTrieRouter", configuration: .init(scalingFactor: .kilo)) { benchmark in
func routerTrieRouterBenchmarks() {
var trie: RouterTrie<String>!
Benchmark("RouterTrieRouter", configuration: .init(scalingFactor: .kilo)) { benchmark in
let testValues = [
"/test/",
"/test/one",
Expand All @@ -39,11 +39,11 @@ func binaryTrieRouterBenchmarks() {
trieBuilder.addEntry("/test/:value/:value2", value: "/test/:value:/:value2")
trieBuilder.addEntry("/api/v1/users/:id/profile", value: "/api/v1/users/:id/profile")
trieBuilder.addEntry("/test2/*/*", value: "/test2/*/*")
trie = BinaryTrie(base: trieBuilder)
trie = RouterTrie(base: trieBuilder)
}

var trie2: BinaryTrie<String>!
Benchmark("BinaryTrieRouterParameters", configuration: .init(scalingFactor: .kilo)) { benchmark in
var trie2: RouterTrie<String>!
Benchmark("RouterTrieRouterParameters", configuration: .init(scalingFactor: .kilo)) { benchmark in
let testValues = [
"/test/value",
"/test/value1/value2",
Expand All @@ -61,11 +61,11 @@ func binaryTrieRouterBenchmarks() {
trieBuilder.addEntry("/test/:value/:value2", value: "/test/:value:/:value2")
trieBuilder.addEntry("/test2/*/*", value: "/test2/*/*")
trieBuilder.addEntry("/api/v1/users/:id/profile", value: "/api/v1/users/:id/profile")
trie2 = BinaryTrie(base: trieBuilder)
trie2 = RouterTrie(base: trieBuilder)
}

var trie3: BinaryTrie<String>!
Benchmark("BinaryTrie:LongPaths", configuration: .init(scalingFactor: .kilo)) { benchmark in
var trie3: RouterTrie<String>!
Benchmark("RouterTrie:LongPaths", configuration: .init(scalingFactor: .kilo)) { benchmark in
let testValues = [
"/api/v1/users/1/profile",
"/api/v1/a/very/long/path/with/lots/of/segments",
Expand All @@ -79,6 +79,6 @@ func binaryTrieRouterBenchmarks() {
let trieBuilder = RouterPathTrieBuilder<String>()
trieBuilder.addEntry("/api/v1/a/very/long/path/with/lots/of/segments", value: "/api/v1/a/very/long/path/with/lots/of/segments")
trieBuilder.addEntry("/api/v1/users/:id/profile", value: "/api/v1/users/:id/profile")
trie3 = BinaryTrie(base: trieBuilder)
trie3 = RouterTrie(base: trieBuilder)
}
}
47 changes: 39 additions & 8 deletions Sources/Hummingbird/Router/Trie/RouterTrie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,61 @@
//
//===----------------------------------------------------------------------===//

enum TrieTokenKind: UInt8 {
case null = 0
case path, capture, prefixCapture, suffixCapture, wildcard, prefixWildcard, suffixWildcard, recursiveWildcard
@usableFromInline
enum TrieToken: Equatable, Sendable {
case null
case path(constantIndex: UInt16)
case capture(parameterIndex: UInt16)
case prefixCapture(parameterIndex: UInt16, suffixIndex: UInt16)
case suffixCapture(prefixIndex: UInt16, parameterIndex: UInt16)
case prefixWildcard(suffixIndex: UInt16)
case suffixWildcard(prefixIndex: UInt16)
case wildcard, recursiveWildcard
case deadEnd
}

@usableFromInline
struct TrieNode: Sendable {
let valueIndex: UInt16
let token: TrieTokenKind
var nextSiblingNodeIndex: UInt16
var constant: UInt16?
var parameter: UInt16?
@usableFromInline
let valueIndex: Int

@usableFromInline
let token: TrieToken

@usableFromInline
var nextSiblingNodeIndex: Int

@usableFromInline
init(valueIndex: Int, token: TrieToken, nextSiblingNodeIndex: Int) {
self.valueIndex = valueIndex
self.token = token
self.nextSiblingNodeIndex = nextSiblingNodeIndex
}
}

@usableFromInline
struct Trie: Sendable {
@usableFromInline
var nodes = [TrieNode]()

@usableFromInline
var parameters = [Substring]()

@usableFromInline
var constants = [Substring]()

@usableFromInline
init() {}
}

@_spi(Internal) public final class RouterTrie<Value: Sendable>: Sendable {
@usableFromInline
let trie: Trie

@usableFromInline
let values: [Value?]

@inlinable
@_spi(Internal) public init(base: RouterPathTrieBuilder<Value>) {
var trie = Trie()
var values: [Value?] = []
Expand Down
96 changes: 31 additions & 65 deletions Sources/Hummingbird/Router/Trie/Trie+resolve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import NIOCore

extension RouterTrie {
/// Resolve a path to a `Value` if available
@inlinable
@_spi(Internal) public func resolve(_ path: String) -> (value: Value, parameters: Parameters)? {
let pathComponents = path.split(separator: "/", omittingEmptySubsequences: true)
var pathComponentsIterator = pathComponents.makeIterator()
Expand Down Expand Up @@ -47,21 +48,16 @@ extension RouterTrie {
}
}

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

/// If `index != nil`, resolves the `index` to a `Value`
/// This is used as a helper in `descendPath(in:parameters:components:)`
private func value(for index: UInt16?, parameters: Parameters) -> (value: Value, parameters: Parameters)? {
if let index, let value = self.values[Int(index)] {
if let value = self.values[node.valueIndex] {
return (value: value, parameters: parameters)
} else {
return nil
}

return nil
}

/// Match sibling node for path component
private func matchComponent(
@usableFromInline
internal func matchComponent(
_ component: Substring,
atNodeIndex nodeIndex: inout Int,
parameters: inout Parameters
Expand Down Expand Up @@ -96,78 +92,48 @@ extension RouterTrie {
parameters: inout Parameters
) -> MatchResult {
switch node.token {
case .path:
case .path(let constant):
// The current node is a constant
guard
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 = node.parameter else {
return .mismatch
if trie.constants[Int(constant)] == component {
return .match
}

return .mismatch
case .capture(let parameter):
parameters[trie.parameters[Int(parameter)]] = component
return .match
case .prefixCapture:
guard
let constant = node.constant,
let parameter = node.parameter
else {
return .mismatch
}
case .prefixCapture(let parameter, let suffix):
let suffix = trie.constants[Int(suffix)]

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 constant = node.constant,
let parameter = node.parameter,
component.hasPrefix(trie.constants[Int(constant)])
else {
return .mismatch
if component.hasSuffix(suffix) {
parameters[trie.parameters[Int(parameter)]] = component.dropLast(suffix.count)
return .match
}

let prefix = trie.constants[Int(constant)]

guard component.hasPrefix(prefix) else {
return .mismatch
return .mismatch
case .suffixCapture(let prefix, let parameter):
let prefix = trie.constants[Int(prefix)]
if component.hasPrefix(prefix) {
parameters[trie.parameters[Int(parameter)]] = component.dropFirst(prefix.count)
return .match
}

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

return .match
case .suffixWildcard:
guard
let constant = node.constant,
component.hasPrefix(trie.constants[Int(constant)])
else {
return .mismatch
return .mismatch
case .suffixWildcard(let prefix):
if component.hasPrefix(trie.constants[Int(prefix)]) {
return .match
}

return .match
return .mismatch
case .recursiveWildcard:
return .match
case .null:
Expand Down
61 changes: 24 additions & 37 deletions Sources/Hummingbird/Router/Trie/Trie+serialize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,86 +15,71 @@
import NIOCore

extension RouterTrie {
@usableFromInline
static func serialize(
_ node: RouterPathTrieBuilder<Value>.Node,
trie: inout Trie,
values: inout [Value?]
) {
// Index where `value` is located
let valueIndex = UInt16(values.count)
let valueIndex = values.count
values.append(node.value)

let token: TrieTokenKind
let constant: UInt16?
let parameter: UInt16?
let token: TrieToken

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

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

switch node.key {
case .path(let path):
token = .path
constant = setConstant(path)
parameter = nil
token = .path(constantIndex: setConstant(path))
case .capture(let parameterName):
token = .capture
constant = nil
parameter = setParameter(parameterName)
token = .capture(parameterIndex: setParameter(parameterName))
case .prefixCapture(suffix: let suffix, parameter: let parameterName):
token = .prefixCapture
constant = setConstant(suffix)
parameter = setParameter(parameterName)
token = .prefixCapture(
parameterIndex: setParameter(parameterName),
suffixIndex: setConstant(suffix)
)
case .suffixCapture(prefix: let prefix, parameter: let parameterName):
token = .suffixCapture
constant = setConstant(prefix)
parameter = setParameter(parameterName)
token = .suffixCapture(
prefixIndex: setConstant(prefix),
parameterIndex: setParameter(parameterName)
)
case .wildcard:
token = .wildcard
constant = nil
parameter = nil
case .prefixWildcard(let suffix):
token = .prefixWildcard
constant = setConstant(suffix)
parameter = nil
token = .prefixWildcard(suffixIndex: setConstant(suffix))
case .suffixWildcard(let prefix):
token = .suffixWildcard
constant = setConstant(prefix)
parameter = nil
token = .suffixWildcard(prefixIndex: setConstant(prefix))
case .recursiveWildcard:
token = .recursiveWildcard
constant = nil
parameter = nil
case .null:
token = .null
constant = nil
parameter = nil
}

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

Expand All @@ -104,9 +89,10 @@ extension RouterTrie {
values: &values
)

trie.nodes[nodeIndex].nextSiblingNodeIndex = UInt16(trie.nodes.count)
trie.nodes[nodeIndex].nextSiblingNodeIndex = trie.nodes.count
}

@usableFromInline
static func serializeChildren(
of node: RouterPathTrieBuilder<Value>.Node,
trie: inout Trie,
Expand All @@ -119,7 +105,8 @@ extension RouterTrie {
}
}

private static func highestPriorityFirst(lhs: RouterPathTrieBuilder<Value>.Node, rhs: RouterPathTrieBuilder<Value>.Node) -> Bool {
@usableFromInline
internal static func highestPriorityFirst(lhs: RouterPathTrieBuilder<Value>.Node, rhs: RouterPathTrieBuilder<Value>.Node) -> Bool {
lhs.key.priority > rhs.key.priority
}
}
Expand Down
Loading

0 comments on commit 1f6d21b

Please sign in to comment.