From 1f6d21b826a0c82a532af7a95d8709da9e1a1169 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Wed, 8 May 2024 18:55:53 +0200 Subject: [PATCH] Even faster! --- Benchmarks/Benchmarks/Router/Benchmarks.swift | 2 +- .../Router/BinaryTrieRouterBenchmarks.swift | 20 ++-- .../Hummingbird/Router/Trie/RouterTrie.swift | 47 +++++++-- .../Router/Trie/Trie+resolve.swift | 96 ++++++------------- .../Router/Trie/Trie+serialize.swift | 61 +++++------- Sources/Hummingbird/Router/TrieRouter.swift | 1 + 6 files changed, 106 insertions(+), 121 deletions(-) diff --git a/Benchmarks/Benchmarks/Router/Benchmarks.swift b/Benchmarks/Benchmarks/Router/Benchmarks.swift index 16875464c..c882f4d49 100644 --- a/Benchmarks/Benchmarks/Router/Benchmarks.swift +++ b/Benchmarks/Benchmarks/Router/Benchmarks.swift @@ -24,6 +24,6 @@ let benchmarks = { ], warmupIterations: 10 ) - binaryTrieRouterBenchmarks() + routerTrieRouterBenchmarks() routerBenchmarks() } diff --git a/Benchmarks/Benchmarks/Router/BinaryTrieRouterBenchmarks.swift b/Benchmarks/Benchmarks/Router/BinaryTrieRouterBenchmarks.swift index 8b6a2199d..bf9205f63 100644 --- a/Benchmarks/Benchmarks/Router/BinaryTrieRouterBenchmarks.swift +++ b/Benchmarks/Benchmarks/Router/BinaryTrieRouterBenchmarks.swift @@ -15,9 +15,9 @@ import Benchmark @_spi(Internal) import Hummingbird -func binaryTrieRouterBenchmarks() { - var trie: BinaryTrie! - Benchmark("BinaryTrieRouter", configuration: .init(scalingFactor: .kilo)) { benchmark in +func routerTrieRouterBenchmarks() { + var trie: RouterTrie! + Benchmark("RouterTrieRouter", configuration: .init(scalingFactor: .kilo)) { benchmark in let testValues = [ "/test/", "/test/one", @@ -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! - Benchmark("BinaryTrieRouterParameters", configuration: .init(scalingFactor: .kilo)) { benchmark in + var trie2: RouterTrie! + Benchmark("RouterTrieRouterParameters", configuration: .init(scalingFactor: .kilo)) { benchmark in let testValues = [ "/test/value", "/test/value1/value2", @@ -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! - Benchmark("BinaryTrie:LongPaths", configuration: .init(scalingFactor: .kilo)) { benchmark in + var trie3: RouterTrie! + 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", @@ -79,6 +79,6 @@ func binaryTrieRouterBenchmarks() { let trieBuilder = RouterPathTrieBuilder() 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) } } diff --git a/Sources/Hummingbird/Router/Trie/RouterTrie.swift b/Sources/Hummingbird/Router/Trie/RouterTrie.swift index d2fd61c25..da9304b07 100644 --- a/Sources/Hummingbird/Router/Trie/RouterTrie.swift +++ b/Sources/Hummingbird/Router/Trie/RouterTrie.swift @@ -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: Sendable { + @usableFromInline let trie: Trie + + @usableFromInline let values: [Value?] + @inlinable @_spi(Internal) public init(base: RouterPathTrieBuilder) { var trie = Trie() var values: [Value?] = [] diff --git a/Sources/Hummingbird/Router/Trie/Trie+resolve.swift b/Sources/Hummingbird/Router/Trie/Trie+resolve.swift index caf931558..d3620099a 100644 --- a/Sources/Hummingbird/Router/Trie/Trie+resolve.swift +++ b/Sources/Hummingbird/Router/Trie/Trie+resolve.swift @@ -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() @@ -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 @@ -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: diff --git a/Sources/Hummingbird/Router/Trie/Trie+serialize.swift b/Sources/Hummingbird/Router/Trie/Trie+serialize.swift index 9c9883b7d..b52ba42b8 100644 --- a/Sources/Hummingbird/Router/Trie/Trie+serialize.swift +++ b/Sources/Hummingbird/Router/Trie/Trie+serialize.swift @@ -15,26 +15,25 @@ import NIOCore extension RouterTrie { + @usableFromInline static func serialize( _ node: RouterPathTrieBuilder.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) } } @@ -42,49 +41,37 @@ extension RouterTrie { 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 @@ -92,9 +79,7 @@ extension RouterTrie { TrieNode( valueIndex: valueIndex, token: token, - nextSiblingNodeIndex: .max, - constant: constant, - parameter: parameter + nextSiblingNodeIndex: .max ) ) @@ -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.Node, trie: inout Trie, @@ -119,7 +105,8 @@ extension RouterTrie { } } - private static func highestPriorityFirst(lhs: RouterPathTrieBuilder.Node, rhs: RouterPathTrieBuilder.Node) -> Bool { + @usableFromInline + internal static func highestPriorityFirst(lhs: RouterPathTrieBuilder.Node, rhs: RouterPathTrieBuilder.Node) -> Bool { lhs.key.priority > rhs.key.priority } } diff --git a/Sources/Hummingbird/Router/TrieRouter.swift b/Sources/Hummingbird/Router/TrieRouter.swift index 400eb5d89..2af29f514 100644 --- a/Sources/Hummingbird/Router/TrieRouter.swift +++ b/Sources/Hummingbird/Router/TrieRouter.swift @@ -16,6 +16,7 @@ import HummingbirdCore /// URI Path Trie Builder @_spi(Internal) public struct RouterPathTrieBuilder { + @usableFromInline var root: Node public init() {