Prechádzať zdrojové kódy

Split path strings using UTF8 (#1027)

Motivation:

For unary calls there can be a lot of path manipulation.
Splitting in utf8 space is approximately 10x faster.

Modifications:

Hand craft a two part split using utf8.

Result:

Very small overall speed improvement (less than 1%)
Peter Adams 5 rokov pred
rodič
commit
d43f2584a0

+ 59 - 4
Sources/GRPC/GRPCServerRequestRoutingHandler.swift

@@ -216,6 +216,37 @@ extension GRPCServerRequestRoutingHandler: ChannelInboundHandler, RemovableChann
     }
   }
 
+  /// A call URI split into components.
+  struct CallPath {
+    /// The name of the service to call.
+    var service: String.UTF8View.SubSequence
+    /// The name of the method to call.
+    var method: String.UTF8View.SubSequence
+
+    /// Charater used to split the path into components.
+    private let pathSplitDelimiter = UInt8(ascii: "/")
+
+    /// Split a path into service and method.
+    /// Split is done in UTF8 as this turns out to be approximately 10x faster than a simple split.
+    /// URI format: "/package.Servicename/MethodName"
+    init?(requestURI: String) {
+      var utf8View = requestURI.utf8[...]
+      // Check and remove the split character at the beginning.
+      guard let prefix = utf8View.trimPrefix(to: self.pathSplitDelimiter), prefix.isEmpty else {
+        return nil
+      }
+      guard let service = utf8View.trimPrefix(to: pathSplitDelimiter) else {
+        return nil
+      }
+      guard let method = utf8View.trimPrefix(to: pathSplitDelimiter) else {
+        return nil
+      }
+
+      self.service = service
+      self.method = method
+    }
+  }
+
   private func makeCallHandler(channel: Channel, requestHead: HTTPRequestHead) -> GRPCCallHandler? {
     // URI format: "/package.Servicename/MethodName", resulting in the following components separated by a slash:
     // - uriComponents[0]: empty
@@ -223,7 +254,7 @@ extension GRPCServerRequestRoutingHandler: ChannelInboundHandler, RemovableChann
     //     `CallHandlerProvider`s should provide the service name including the package name.
     // - uriComponents[2]: method name.
     self.logger.debug("making call handler", metadata: ["path": "\(requestHead.uri)"])
-    let uriComponents = requestHead.uri.split(separator: "/")
+    let uriComponents = CallPath(requestURI: requestHead.uri)
 
     let context = CallHandlerContext(
       errorDelegate: self.errorDelegate,
@@ -231,10 +262,10 @@ extension GRPCServerRequestRoutingHandler: ChannelInboundHandler, RemovableChann
       encoding: self.encoding
     )
 
-    guard uriComponents.count >= 2,
-      let providerForServiceName = servicesByName[uriComponents[0]],
+    guard let callPath = uriComponents,
+      let providerForServiceName = servicesByName[String.SubSequence(callPath.service)],
       let callHandler = providerForServiceName.handleMethod(
-        uriComponents[1],
+        String.SubSequence(callPath.method),
         callHandlerContext: context
       ) else {
       self.logger.notice("could not create handler", metadata: ["path": "\(requestHead.uri)"])
@@ -257,3 +288,27 @@ extension GRPCServerRequestRoutingHandler: ChannelInboundHandler, RemovableChann
     return HTTPResponseHead(version: requestHead.version, status: .ok, headers: headers)
   }
 }
+
+extension Collection where Self == Self.SubSequence, Self.Element: Equatable {
+  /// Trims out the prefix up to `separator`, and returns it.
+  /// Sets self to the subsequence after the separator, and returns the subsequence before the separator.
+  /// If self is emtpy returns `nil`
+  /// - parameters:
+  ///     - separator : The Element between the head which is returned and the rest which is left in self.
+  /// - returns: SubSequence containing everything between the beginnning and the first occurance of
+  /// `separator`.  If `separator` is not found this will be the entire Collection. If the collection is empty
+  /// returns `nil`
+  mutating func trimPrefix(to separator: Element) -> SubSequence? {
+    guard !self.isEmpty else {
+      return nil
+    }
+    if let separatorIndex = self.firstIndex(of: separator) {
+      let indexAfterSeparator = self.index(after: separatorIndex)
+      defer { self = self[indexAfterSeparator...] }
+      return self[..<separatorIndex]
+    } else {
+      defer { self = self[self.endIndex...] }
+      return self[...]
+    }
+  }
+}

+ 49 - 1
Tests/GRPCTests/GRPCServerRequestRoutingHandlerTests.swift

@@ -16,7 +16,7 @@
 import EchoImplementation
 import EchoModel
 import Foundation
-import GRPC
+@testable import GRPC
 import Logging
 import NIO
 import NIOHTTP1
@@ -121,4 +121,52 @@ class GRPCServerRequestRoutingHandlerTests: GRPCTestCase {
       .handler(type: UnaryCallHandler<Echo_EchoRequest, Echo_EchoResponse>.self)
     XCTAssertNoThrow(try unary.wait())
   }
+
+  func testSplitPathNormal() {
+    let path = "/server/method"
+    let parsedPath = GRPCServerRequestRoutingHandler.CallPath(requestURI: path)
+    let splitPath = path.split(separator: "/")
+
+    XCTAssertEqual(splitPath[0], String.SubSequence(parsedPath!.service))
+    XCTAssertEqual(splitPath[1], String.SubSequence(parsedPath!.method))
+  }
+
+  func testSplitPathTooShort() {
+    let path = "/badPath"
+    let parsedPath = GRPCServerRequestRoutingHandler.CallPath(requestURI: path)
+
+    XCTAssertNil(parsedPath)
+  }
+
+  func testSplitPathTooLong() {
+    let path = "/server/method/discard"
+    let parsedPath = GRPCServerRequestRoutingHandler.CallPath(requestURI: path)
+    let splitPath = path.split(separator: "/")
+
+    XCTAssertEqual(splitPath[0], String.SubSequence(parsedPath!.service))
+    XCTAssertEqual(splitPath[1], String.SubSequence(parsedPath!.method))
+  }
+
+  func testTrimPrefixEmpty() {
+    var toSplit = "".utf8[...]
+    let head = toSplit.trimPrefix(to: UInt8(ascii: "/"))
+    XCTAssertNil(head)
+    XCTAssertEqual(toSplit.count, 0)
+  }
+
+  func testTrimPrefixAll() {
+    let source = "words"
+    var toSplit = source.utf8[...]
+    let head = toSplit.trimPrefix(to: UInt8(ascii: "/"))
+    XCTAssertEqual(head?.count, source.utf8.count)
+    XCTAssertEqual(toSplit.count, 0)
+  }
+
+  func testTrimPrefixAndRest() {
+    let source = "words/moreWords"
+    var toSplit = source.utf8[...]
+    let head = toSplit.trimPrefix(to: UInt8(ascii: "/"))
+    XCTAssertEqual(head?.count, "words".utf8.count)
+    XCTAssertEqual(toSplit.count, "moreWords".utf8.count)
+  }
 }

+ 6 - 0
Tests/GRPCTests/XCTestManifests.swift

@@ -594,6 +594,12 @@ extension GRPCServerRequestRoutingHandlerTests {
     static let __allTests__GRPCServerRequestRoutingHandlerTests = [
         ("testImplementedMethodReconfiguresPipeline", testImplementedMethodReconfiguresPipeline),
         ("testInvalidGRPCContentTypeReturnsUnsupportedMediaType", testInvalidGRPCContentTypeReturnsUnsupportedMediaType),
+        ("testSplitPathNormal", testSplitPathNormal),
+        ("testSplitPathTooLong", testSplitPathTooLong),
+        ("testSplitPathTooShort", testSplitPathTooShort),
+        ("testTrimPrefixAll", testTrimPrefixAll),
+        ("testTrimPrefixAndRest", testTrimPrefixAndRest),
+        ("testTrimPrefixEmpty", testTrimPrefixEmpty),
         ("testUnimplementedMethodReturnsUnimplementedStatus", testUnimplementedMethodReturnsUnimplementedStatus),
     ]
 }